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

     1  package consistencytestutil
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io"
     7  
     8  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
     9  	"google.golang.org/grpc"
    10  	"google.golang.org/protobuf/types/known/structpb"
    11  
    12  	v1svc "github.com/authzed/spicedb/internal/services/v1"
    13  	"github.com/authzed/spicedb/pkg/datastore"
    14  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    15  	"github.com/authzed/spicedb/pkg/tuple"
    16  	"github.com/authzed/spicedb/pkg/zedtoken"
    17  )
    18  
    19  func ServiceTesters(conn *grpc.ClientConn) []ServiceTester {
    20  	return []ServiceTester{
    21  		v1ServiceTester{v1.NewPermissionsServiceClient(conn), v1.NewExperimentalServiceClient(conn)},
    22  	}
    23  }
    24  
    25  type ServiceTester interface {
    26  	Name() string
    27  	Check(ctx context.Context, resource *core.ObjectAndRelation, subject *core.ObjectAndRelation, atRevision datastore.Revision, caveatContext map[string]any) (v1.CheckPermissionResponse_Permissionship, error)
    28  	Expand(ctx context.Context, resource *core.ObjectAndRelation, atRevision datastore.Revision) (*core.RelationTupleTreeNode, error)
    29  	Write(ctx context.Context, relationship *core.RelationTuple) error
    30  	Read(ctx context.Context, namespaceName string, atRevision datastore.Revision) ([]*core.RelationTuple, error)
    31  	LookupResources(ctx context.Context, resourceRelation *core.RelationReference, subject *core.ObjectAndRelation, atRevision datastore.Revision, cursor *v1.Cursor, limit uint32) ([]*v1.LookupResourcesResponse, *v1.Cursor, error)
    32  	LookupSubjects(ctx context.Context, resource *core.ObjectAndRelation, subjectRelation *core.RelationReference, atRevision datastore.Revision, caveatContext map[string]any, cursor *v1.Cursor, limit uint32) (map[string]*v1.LookupSubjectsResponse, *v1.Cursor, error)
    33  	// NOTE: ExperimentalService/BulkCheckPermission has been promoted to PermissionsService/CheckBulkPermissions
    34  	BulkCheck(ctx context.Context, items []*v1.BulkCheckPermissionRequestItem, atRevision datastore.Revision) ([]*v1.BulkCheckPermissionPair, error)
    35  	CheckBulk(ctx context.Context, items []*v1.CheckBulkPermissionsRequestItem, atRevision datastore.Revision) ([]*v1.CheckBulkPermissionsPair, error)
    36  }
    37  
    38  func optionalizeRelation(relation string) string {
    39  	if relation == datastore.Ellipsis {
    40  		return ""
    41  	}
    42  
    43  	return relation
    44  }
    45  
    46  // v1ServiceTester tests the V1 API.
    47  type v1ServiceTester struct {
    48  	permClient v1.PermissionsServiceClient
    49  	expClient  v1.ExperimentalServiceClient
    50  }
    51  
    52  func (v1st v1ServiceTester) Name() string {
    53  	return "v1"
    54  }
    55  
    56  func (v1st v1ServiceTester) Check(ctx context.Context, resource *core.ObjectAndRelation, subject *core.ObjectAndRelation, atRevision datastore.Revision, caveatContext map[string]any) (v1.CheckPermissionResponse_Permissionship, error) {
    57  	var context *structpb.Struct
    58  	if caveatContext != nil {
    59  		built, err := structpb.NewStruct(caveatContext)
    60  		if err != nil {
    61  			return v1.CheckPermissionResponse_PERMISSIONSHIP_UNSPECIFIED, err
    62  		}
    63  		context = built
    64  	}
    65  
    66  	checkResp, err := v1st.permClient.CheckPermission(ctx, &v1.CheckPermissionRequest{
    67  		Resource: &v1.ObjectReference{
    68  			ObjectType: resource.Namespace,
    69  			ObjectId:   resource.ObjectId,
    70  		},
    71  		Permission: resource.Relation,
    72  		Subject: &v1.SubjectReference{
    73  			Object: &v1.ObjectReference{
    74  				ObjectType: subject.Namespace,
    75  				ObjectId:   subject.ObjectId,
    76  			},
    77  			OptionalRelation: optionalizeRelation(subject.Relation),
    78  		},
    79  		Consistency: &v1.Consistency{
    80  			Requirement: &v1.Consistency_AtLeastAsFresh{
    81  				AtLeastAsFresh: zedtoken.MustNewFromRevision(atRevision),
    82  			},
    83  		},
    84  		Context: context,
    85  	})
    86  	if err != nil {
    87  		return v1.CheckPermissionResponse_PERMISSIONSHIP_UNSPECIFIED, err
    88  	}
    89  	return checkResp.Permissionship, nil
    90  }
    91  
    92  func (v1st v1ServiceTester) Expand(ctx context.Context, resource *core.ObjectAndRelation, atRevision datastore.Revision) (*core.RelationTupleTreeNode, error) {
    93  	expandResp, err := v1st.permClient.ExpandPermissionTree(ctx, &v1.ExpandPermissionTreeRequest{
    94  		Resource: &v1.ObjectReference{
    95  			ObjectType: resource.Namespace,
    96  			ObjectId:   resource.ObjectId,
    97  		},
    98  		Permission: resource.Relation,
    99  		Consistency: &v1.Consistency{
   100  			Requirement: &v1.Consistency_AtLeastAsFresh{
   101  				AtLeastAsFresh: zedtoken.MustNewFromRevision(atRevision),
   102  			},
   103  		},
   104  	})
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  	return v1svc.TranslateRelationshipTree(expandResp.TreeRoot), nil
   109  }
   110  
   111  func (v1st v1ServiceTester) Write(ctx context.Context, relationship *core.RelationTuple) error {
   112  	_, err := v1st.permClient.WriteRelationships(ctx, &v1.WriteRelationshipsRequest{
   113  		OptionalPreconditions: []*v1.Precondition{
   114  			{
   115  				Operation: v1.Precondition_OPERATION_MUST_MATCH,
   116  				Filter:    tuple.MustToFilter(relationship),
   117  			},
   118  		},
   119  		Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Touch(relationship))},
   120  	})
   121  	return err
   122  }
   123  
   124  func (v1st v1ServiceTester) Read(_ context.Context, namespaceName string, atRevision datastore.Revision) ([]*core.RelationTuple, error) {
   125  	readResp, err := v1st.permClient.ReadRelationships(context.Background(), &v1.ReadRelationshipsRequest{
   126  		RelationshipFilter: &v1.RelationshipFilter{
   127  			ResourceType: namespaceName,
   128  		},
   129  		Consistency: &v1.Consistency{
   130  			Requirement: &v1.Consistency_AtLeastAsFresh{
   131  				AtLeastAsFresh: zedtoken.MustNewFromRevision(atRevision),
   132  			},
   133  		},
   134  	})
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	var tuples []*core.RelationTuple
   140  	for {
   141  		resp, err := readResp.Recv()
   142  		if errors.Is(err, io.EOF) {
   143  			break
   144  		}
   145  
   146  		if err != nil {
   147  			return nil, err
   148  		}
   149  
   150  		tuples = append(tuples, tuple.MustFromRelationship[*v1.ObjectReference, *v1.SubjectReference, *v1.ContextualizedCaveat](resp.Relationship))
   151  	}
   152  
   153  	return tuples, nil
   154  }
   155  
   156  func (v1st v1ServiceTester) LookupResources(_ context.Context, resourceRelation *core.RelationReference, subject *core.ObjectAndRelation, atRevision datastore.Revision, cursor *v1.Cursor, limit uint32) ([]*v1.LookupResourcesResponse, *v1.Cursor, error) {
   157  	lookupResp, err := v1st.permClient.LookupResources(context.Background(), &v1.LookupResourcesRequest{
   158  		ResourceObjectType: resourceRelation.Namespace,
   159  		Permission:         resourceRelation.Relation,
   160  		Subject: &v1.SubjectReference{
   161  			Object: &v1.ObjectReference{
   162  				ObjectType: subject.Namespace,
   163  				ObjectId:   subject.ObjectId,
   164  			},
   165  			OptionalRelation: optionalizeRelation(subject.Relation),
   166  		},
   167  		Consistency: &v1.Consistency{
   168  			Requirement: &v1.Consistency_AtLeastAsFresh{
   169  				AtLeastAsFresh: zedtoken.MustNewFromRevision(atRevision),
   170  			},
   171  		},
   172  		OptionalLimit:  limit,
   173  		OptionalCursor: cursor,
   174  	})
   175  	if err != nil {
   176  		return nil, nil, err
   177  	}
   178  
   179  	var lastCursor *v1.Cursor
   180  	found := []*v1.LookupResourcesResponse{}
   181  	for {
   182  		resp, err := lookupResp.Recv()
   183  		if errors.Is(err, io.EOF) {
   184  			break
   185  		}
   186  
   187  		if err != nil {
   188  			return nil, nil, err
   189  		}
   190  
   191  		found = append(found, resp)
   192  		lastCursor = resp.AfterResultCursor
   193  	}
   194  	return found, lastCursor, nil
   195  }
   196  
   197  func (v1st v1ServiceTester) LookupSubjects(_ context.Context, resource *core.ObjectAndRelation, subjectRelation *core.RelationReference, atRevision datastore.Revision, caveatContext map[string]any, cursor *v1.Cursor, limit uint32) (map[string]*v1.LookupSubjectsResponse, *v1.Cursor, error) {
   198  	var builtContext *structpb.Struct
   199  	if caveatContext != nil {
   200  		built, err := structpb.NewStruct(caveatContext)
   201  		if err != nil {
   202  			return nil, nil, err
   203  		}
   204  		builtContext = built
   205  	}
   206  
   207  	lookupResp, err := v1st.permClient.LookupSubjects(context.Background(), &v1.LookupSubjectsRequest{
   208  		Resource: &v1.ObjectReference{
   209  			ObjectType: resource.Namespace,
   210  			ObjectId:   resource.ObjectId,
   211  		},
   212  		Permission:              resource.Relation,
   213  		SubjectObjectType:       subjectRelation.Namespace,
   214  		OptionalSubjectRelation: optionalizeRelation(subjectRelation.Relation),
   215  		Consistency: &v1.Consistency{
   216  			Requirement: &v1.Consistency_AtLeastAsFresh{
   217  				AtLeastAsFresh: zedtoken.MustNewFromRevision(atRevision),
   218  			},
   219  		},
   220  		Context:               builtContext,
   221  		OptionalCursor:        cursor,
   222  		OptionalConcreteLimit: limit,
   223  	})
   224  	if err != nil {
   225  		return nil, nil, err
   226  	}
   227  
   228  	found := map[string]*v1.LookupSubjectsResponse{}
   229  	var lastCursor *v1.Cursor
   230  	for {
   231  		resp, err := lookupResp.Recv()
   232  		if errors.Is(err, io.EOF) {
   233  			break
   234  		}
   235  
   236  		if err != nil {
   237  			return nil, nil, err
   238  		}
   239  
   240  		found[resp.Subject.SubjectObjectId] = resp
   241  		lastCursor = resp.AfterResultCursor
   242  	}
   243  	return found, lastCursor, nil
   244  }
   245  
   246  func (v1st v1ServiceTester) BulkCheck(ctx context.Context, items []*v1.BulkCheckPermissionRequestItem, atRevision datastore.Revision) ([]*v1.BulkCheckPermissionPair, error) {
   247  	result, err := v1st.expClient.BulkCheckPermission(ctx, &v1.BulkCheckPermissionRequest{
   248  		Items: items,
   249  		Consistency: &v1.Consistency{
   250  			Requirement: &v1.Consistency_AtLeastAsFresh{
   251  				AtLeastAsFresh: zedtoken.MustNewFromRevision(atRevision),
   252  			},
   253  		},
   254  	})
   255  	if err != nil {
   256  		return nil, err
   257  	}
   258  
   259  	return result.Pairs, nil
   260  }
   261  
   262  func (v1st v1ServiceTester) CheckBulk(ctx context.Context, items []*v1.CheckBulkPermissionsRequestItem, atRevision datastore.Revision) ([]*v1.CheckBulkPermissionsPair, error) {
   263  	result, err := v1st.permClient.CheckBulkPermissions(ctx, &v1.CheckBulkPermissionsRequest{
   264  		Items: items,
   265  		Consistency: &v1.Consistency{
   266  			Requirement: &v1.Consistency_AtLeastAsFresh{
   267  				AtLeastAsFresh: zedtoken.MustNewFromRevision(atRevision),
   268  			},
   269  		},
   270  	})
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	return result.Pairs, nil
   276  }