github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/tuple/tuple.go (about)

     1  package tuple
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"maps"
     7  	"reflect"
     8  	"regexp"
     9  	"slices"
    10  
    11  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
    12  	"github.com/jzelinskie/stringz"
    13  	"google.golang.org/protobuf/proto"
    14  	"google.golang.org/protobuf/types/known/structpb"
    15  
    16  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    17  )
    18  
    19  const (
    20  	// Ellipsis is the Ellipsis relation in v0 style subjects.
    21  	Ellipsis = "..."
    22  
    23  	// PublicWildcard is the wildcard value for subject object IDs that indicates public access
    24  	// for the subject type.
    25  	PublicWildcard = "*"
    26  )
    27  
    28  const (
    29  	namespaceNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]"
    30  	resourceIDExpr    = "([a-zA-Z0-9/_|\\-=+]{1,})"
    31  	subjectIDExpr     = "([a-zA-Z0-9/_|\\-=+]{1,})|\\*"
    32  	relationExpr      = "[a-z][a-z0-9_]{1,62}[a-z0-9]"
    33  	caveatNameExpr    = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]"
    34  )
    35  
    36  var onrExpr = fmt.Sprintf(
    37  	`(?P<resourceType>(%s)):(?P<resourceID>%s)#(?P<resourceRel>%s)`,
    38  	namespaceNameExpr,
    39  	resourceIDExpr,
    40  	relationExpr,
    41  )
    42  
    43  var subjectExpr = fmt.Sprintf(
    44  	`(?P<subjectType>(%s)):(?P<subjectID>%s)(#(?P<subjectRel>%s|\.\.\.))?`,
    45  	namespaceNameExpr,
    46  	subjectIDExpr,
    47  	relationExpr,
    48  )
    49  
    50  var caveatExpr = fmt.Sprintf(`\[(?P<caveatName>(%s))(:(?P<caveatContext>(\{(.+)\})))?\]`, caveatNameExpr)
    51  
    52  var (
    53  	onrRegex        = regexp.MustCompile(fmt.Sprintf("^%s$", onrExpr))
    54  	subjectRegex    = regexp.MustCompile(fmt.Sprintf("^%s$", subjectExpr))
    55  	resourceIDRegex = regexp.MustCompile(fmt.Sprintf("^%s$", resourceIDExpr))
    56  	subjectIDRegex  = regexp.MustCompile(fmt.Sprintf("^%s$", subjectIDExpr))
    57  )
    58  
    59  var parserRegex = regexp.MustCompile(
    60  	fmt.Sprintf(
    61  		`^%s@%s(%s)?$`,
    62  		onrExpr,
    63  		subjectExpr,
    64  		caveatExpr,
    65  	),
    66  )
    67  
    68  // ValidateResourceID ensures that the given resource ID is valid. Returns an error if not.
    69  func ValidateResourceID(objectID string) error {
    70  	if !resourceIDRegex.MatchString(objectID) {
    71  		return fmt.Errorf("invalid resource id; must match %s", resourceIDExpr)
    72  	}
    73  	if len(objectID) > 1024 {
    74  		return fmt.Errorf("invalid resource id; must be <= 1024 characters")
    75  	}
    76  
    77  	return nil
    78  }
    79  
    80  // ValidateSubjectID ensures that the given object ID (under a subject reference) is valid. Returns an error if not.
    81  func ValidateSubjectID(subjectID string) error {
    82  	if !subjectIDRegex.MatchString(subjectID) {
    83  		return fmt.Errorf("invalid subject id; must be alphanumeric and between 1 and 127 characters or a star for public")
    84  	}
    85  	if len(subjectID) > 1024 {
    86  		return fmt.Errorf("invalid resource id; must be <= 1024 characters")
    87  	}
    88  
    89  	return nil
    90  }
    91  
    92  // MustString converts a tuple to a string. If the tuple is nil or empty, returns empty string.
    93  func MustString(tpl *core.RelationTuple) string {
    94  	tplString, err := String(tpl)
    95  	if err != nil {
    96  		panic(err)
    97  	}
    98  	return tplString
    99  }
   100  
   101  // String converts a tuple to a string. If the tuple is nil or empty, returns empty string.
   102  func String(tpl *core.RelationTuple) (string, error) {
   103  	if tpl == nil || tpl.ResourceAndRelation == nil || tpl.Subject == nil {
   104  		return "", nil
   105  	}
   106  
   107  	caveatString, err := StringCaveat(tpl.Caveat)
   108  	if err != nil {
   109  		return "", err
   110  	}
   111  
   112  	return fmt.Sprintf("%s@%s%s", StringONR(tpl.ResourceAndRelation), StringONR(tpl.Subject), caveatString), nil
   113  }
   114  
   115  // StringWithoutCaveat converts a tuple to a string, without its caveat included.
   116  func StringWithoutCaveat(tpl *core.RelationTuple) string {
   117  	if tpl == nil || tpl.ResourceAndRelation == nil || tpl.Subject == nil {
   118  		return ""
   119  	}
   120  
   121  	return fmt.Sprintf("%s@%s", StringONR(tpl.ResourceAndRelation), StringONR(tpl.Subject))
   122  }
   123  
   124  func MustStringCaveat(caveat *core.ContextualizedCaveat) string {
   125  	caveatString, err := StringCaveat(caveat)
   126  	if err != nil {
   127  		panic(err)
   128  	}
   129  	return caveatString
   130  }
   131  
   132  // StringCaveat converts a contextualized caveat to a string. If the caveat is nil or empty, returns empty string.
   133  func StringCaveat(caveat *core.ContextualizedCaveat) (string, error) {
   134  	if caveat == nil || caveat.CaveatName == "" {
   135  		return "", nil
   136  	}
   137  
   138  	contextString, err := StringCaveatContext(caveat.Context)
   139  	if err != nil {
   140  		return "", err
   141  	}
   142  
   143  	if len(contextString) > 0 {
   144  		contextString = ":" + contextString
   145  	}
   146  
   147  	return fmt.Sprintf("[%s%s]", caveat.CaveatName, contextString), nil
   148  }
   149  
   150  // StringCaveatContext converts the context of a caveat to a string. If the context is nil or empty, returns an empty string.
   151  func StringCaveatContext(context *structpb.Struct) (string, error) {
   152  	if context == nil || len(context.Fields) == 0 {
   153  		return "", nil
   154  	}
   155  
   156  	contextBytes, err := context.MarshalJSON()
   157  	if err != nil {
   158  		return "", err
   159  	}
   160  	return string(contextBytes), nil
   161  }
   162  
   163  // MustRelString converts a relationship into a string.  Will panic if
   164  // the Relationship does not validate.
   165  func MustRelString(rel *v1.Relationship) string {
   166  	if err := rel.Validate(); err != nil {
   167  		panic(fmt.Sprintf("invalid relationship: %#v %s", rel, err))
   168  	}
   169  	return MustStringRelationship(rel)
   170  }
   171  
   172  // MustParse wraps Parse such that any failures panic rather than returning
   173  // nil.
   174  func MustParse(tpl string) *core.RelationTuple {
   175  	if parsed := Parse(tpl); parsed != nil {
   176  		return parsed
   177  	}
   178  	panic("failed to parse tuple")
   179  }
   180  
   181  // Parse unmarshals the string form of a Tuple and returns nil if there is a
   182  // failure.
   183  //
   184  // This function treats both missing and Ellipsis relations equally.
   185  func Parse(tpl string) *core.RelationTuple {
   186  	groups := parserRegex.FindStringSubmatch(tpl)
   187  	if len(groups) == 0 {
   188  		return nil
   189  	}
   190  
   191  	subjectRelation := Ellipsis
   192  	subjectRelIndex := slices.Index(parserRegex.SubexpNames(), "subjectRel")
   193  	if len(groups[subjectRelIndex]) > 0 {
   194  		subjectRelation = groups[subjectRelIndex]
   195  	}
   196  
   197  	caveatName := groups[slices.Index(parserRegex.SubexpNames(), "caveatName")]
   198  	var optionalCaveat *core.ContextualizedCaveat
   199  	if caveatName != "" {
   200  		optionalCaveat = &core.ContextualizedCaveat{
   201  			CaveatName: caveatName,
   202  		}
   203  
   204  		caveatContextString := groups[slices.Index(parserRegex.SubexpNames(), "caveatContext")]
   205  		if len(caveatContextString) > 0 {
   206  			contextMap := make(map[string]any, 1)
   207  			err := json.Unmarshal([]byte(caveatContextString), &contextMap)
   208  			if err != nil {
   209  				return nil
   210  			}
   211  
   212  			caveatContext, err := structpb.NewStruct(contextMap)
   213  			if err != nil {
   214  				return nil
   215  			}
   216  
   217  			optionalCaveat.Context = caveatContext
   218  		}
   219  	}
   220  
   221  	resourceID := groups[slices.Index(parserRegex.SubexpNames(), "resourceID")]
   222  	if err := ValidateResourceID(resourceID); err != nil {
   223  		return nil
   224  	}
   225  
   226  	subjectID := groups[slices.Index(parserRegex.SubexpNames(), "subjectID")]
   227  	if err := ValidateSubjectID(subjectID); err != nil {
   228  		return nil
   229  	}
   230  
   231  	return &core.RelationTuple{
   232  		ResourceAndRelation: &core.ObjectAndRelation{
   233  			Namespace: groups[slices.Index(parserRegex.SubexpNames(), "resourceType")],
   234  			ObjectId:  resourceID,
   235  			Relation:  groups[slices.Index(parserRegex.SubexpNames(), "resourceRel")],
   236  		},
   237  		Subject: &core.ObjectAndRelation{
   238  			Namespace: groups[slices.Index(parserRegex.SubexpNames(), "subjectType")],
   239  			ObjectId:  subjectID,
   240  			Relation:  subjectRelation,
   241  		},
   242  		Caveat: optionalCaveat,
   243  	}
   244  }
   245  
   246  func ParseRel(rel string) *v1.Relationship {
   247  	tpl := Parse(rel)
   248  	if tpl == nil {
   249  		return nil
   250  	}
   251  	return ToRelationship(tpl)
   252  }
   253  
   254  func Create(tpl *core.RelationTuple) *core.RelationTupleUpdate {
   255  	return &core.RelationTupleUpdate{
   256  		Operation: core.RelationTupleUpdate_CREATE,
   257  		Tuple:     tpl,
   258  	}
   259  }
   260  
   261  func Touch(tpl *core.RelationTuple) *core.RelationTupleUpdate {
   262  	return &core.RelationTupleUpdate{
   263  		Operation: core.RelationTupleUpdate_TOUCH,
   264  		Tuple:     tpl,
   265  	}
   266  }
   267  
   268  func Delete(tpl *core.RelationTuple) *core.RelationTupleUpdate {
   269  	return &core.RelationTupleUpdate{
   270  		Operation: core.RelationTupleUpdate_DELETE,
   271  		Tuple:     tpl,
   272  	}
   273  }
   274  
   275  // Equal returns true if the two relationships are exactly the same.
   276  func Equal(lhs, rhs *core.RelationTuple) bool {
   277  	return OnrEqual(lhs.ResourceAndRelation, rhs.ResourceAndRelation) && OnrEqual(lhs.Subject, rhs.Subject) && caveatEqual(lhs.Caveat, rhs.Caveat)
   278  }
   279  
   280  func caveatEqual(lhs, rhs *core.ContextualizedCaveat) bool {
   281  	if lhs == nil && rhs == nil {
   282  		return true
   283  	}
   284  
   285  	if lhs == nil || rhs == nil {
   286  		return false
   287  	}
   288  
   289  	return lhs.CaveatName == rhs.CaveatName && proto.Equal(lhs.Context, rhs.Context)
   290  }
   291  
   292  // MustToRelationship converts a RelationTuple into a Relationship. Will panic if
   293  // the RelationTuple does not validate.
   294  func MustToRelationship(tpl *core.RelationTuple) *v1.Relationship {
   295  	if err := tpl.Validate(); err != nil {
   296  		panic(fmt.Sprintf("invalid tuple: %#v %s", tpl, err))
   297  	}
   298  
   299  	return ToRelationship(tpl)
   300  }
   301  
   302  // ToRelationship converts a RelationTuple into a Relationship.
   303  func ToRelationship(tpl *core.RelationTuple) *v1.Relationship {
   304  	var caveat *v1.ContextualizedCaveat
   305  	if tpl.Caveat != nil {
   306  		caveat = &v1.ContextualizedCaveat{
   307  			CaveatName: tpl.Caveat.CaveatName,
   308  			Context:    tpl.Caveat.Context,
   309  		}
   310  	}
   311  	return &v1.Relationship{
   312  		Resource: &v1.ObjectReference{
   313  			ObjectType: tpl.ResourceAndRelation.Namespace,
   314  			ObjectId:   tpl.ResourceAndRelation.ObjectId,
   315  		},
   316  		Relation: tpl.ResourceAndRelation.Relation,
   317  		Subject: &v1.SubjectReference{
   318  			Object: &v1.ObjectReference{
   319  				ObjectType: tpl.Subject.Namespace,
   320  				ObjectId:   tpl.Subject.ObjectId,
   321  			},
   322  			OptionalRelation: stringz.Default(tpl.Subject.Relation, "", Ellipsis),
   323  		},
   324  		OptionalCaveat: caveat,
   325  	}
   326  }
   327  
   328  // NewRelationship creates a new Relationship value with all its required child structures allocated
   329  func NewRelationship() *v1.Relationship {
   330  	return &v1.Relationship{
   331  		Resource: &v1.ObjectReference{},
   332  		Subject: &v1.SubjectReference{
   333  			Object: &v1.ObjectReference{},
   334  		},
   335  	}
   336  }
   337  
   338  // MustToRelationshipMutating sets target relationship to all the values provided in the source tuple.
   339  func MustToRelationshipMutating(source *core.RelationTuple, targetRel *v1.Relationship, targetCaveat *v1.ContextualizedCaveat) {
   340  	targetRel.Resource.ObjectType = source.ResourceAndRelation.Namespace
   341  	targetRel.Resource.ObjectId = source.ResourceAndRelation.ObjectId
   342  	targetRel.Relation = source.ResourceAndRelation.Relation
   343  	targetRel.Subject.Object.ObjectType = source.Subject.Namespace
   344  	targetRel.Subject.Object.ObjectId = source.Subject.ObjectId
   345  	targetRel.Subject.OptionalRelation = stringz.Default(source.Subject.Relation, "", Ellipsis)
   346  	targetRel.OptionalCaveat = nil
   347  
   348  	if source.Caveat != nil {
   349  		if targetCaveat == nil {
   350  			panic("expected a provided target caveat")
   351  		}
   352  		targetCaveat.CaveatName = source.Caveat.CaveatName
   353  		targetCaveat.Context = source.Caveat.Context
   354  		targetRel.OptionalCaveat = targetCaveat
   355  	}
   356  }
   357  
   358  // MustToFilter converts a RelationTuple into a RelationshipFilter. Will panic if
   359  // the RelationTuple does not validate.
   360  func MustToFilter(tpl *core.RelationTuple) *v1.RelationshipFilter {
   361  	if err := tpl.Validate(); err != nil {
   362  		panic(fmt.Sprintf("invalid tuple: %#v %s", tpl, err))
   363  	}
   364  
   365  	return ToFilter(tpl)
   366  }
   367  
   368  // ToFilter converts a RelationTuple into a RelationshipFilter.
   369  func ToFilter(tpl *core.RelationTuple) *v1.RelationshipFilter {
   370  	return &v1.RelationshipFilter{
   371  		ResourceType:          tpl.ResourceAndRelation.Namespace,
   372  		OptionalResourceId:    tpl.ResourceAndRelation.ObjectId,
   373  		OptionalRelation:      tpl.ResourceAndRelation.Relation,
   374  		OptionalSubjectFilter: UsersetToSubjectFilter(tpl.Subject),
   375  	}
   376  }
   377  
   378  // UsersetToSubjectFilter converts a userset to the equivalent exact SubjectFilter.
   379  func UsersetToSubjectFilter(userset *core.ObjectAndRelation) *v1.SubjectFilter {
   380  	return &v1.SubjectFilter{
   381  		SubjectType:       userset.Namespace,
   382  		OptionalSubjectId: userset.ObjectId,
   383  		OptionalRelation: &v1.SubjectFilter_RelationFilter{
   384  			Relation: stringz.Default(userset.Relation, "", Ellipsis),
   385  		},
   386  	}
   387  }
   388  
   389  // RelToFilter converts a Relationship into a RelationshipFilter.
   390  func RelToFilter(rel *v1.Relationship) *v1.RelationshipFilter {
   391  	return &v1.RelationshipFilter{
   392  		ResourceType:       rel.Resource.ObjectType,
   393  		OptionalResourceId: rel.Resource.ObjectId,
   394  		OptionalRelation:   rel.Relation,
   395  		OptionalSubjectFilter: &v1.SubjectFilter{
   396  			SubjectType:       rel.Subject.Object.ObjectType,
   397  			OptionalSubjectId: rel.Subject.Object.ObjectId,
   398  			OptionalRelation: &v1.SubjectFilter_RelationFilter{
   399  				Relation: rel.Subject.OptionalRelation,
   400  			},
   401  		},
   402  	}
   403  }
   404  
   405  // UpdatesToRelationshipUpdates converts a slice of RelationTupleUpdate into a
   406  // slice of RelationshipUpdate.
   407  func UpdatesToRelationshipUpdates(updates []*core.RelationTupleUpdate) []*v1.RelationshipUpdate {
   408  	relationshipUpdates := make([]*v1.RelationshipUpdate, 0, len(updates))
   409  
   410  	for _, update := range updates {
   411  		relationshipUpdates = append(relationshipUpdates, UpdateToRelationshipUpdate(update))
   412  	}
   413  
   414  	return relationshipUpdates
   415  }
   416  
   417  func UpdateFromRelationshipUpdates(updates []*v1.RelationshipUpdate) []*core.RelationTupleUpdate {
   418  	relationshipUpdates := make([]*core.RelationTupleUpdate, 0, len(updates))
   419  
   420  	for _, update := range updates {
   421  		relationshipUpdates = append(relationshipUpdates, UpdateFromRelationshipUpdate(update))
   422  	}
   423  
   424  	return relationshipUpdates
   425  }
   426  
   427  // UpdateToRelationshipUpdate converts a RelationTupleUpdate into a
   428  // RelationshipUpdate.
   429  func UpdateToRelationshipUpdate(update *core.RelationTupleUpdate) *v1.RelationshipUpdate {
   430  	var op v1.RelationshipUpdate_Operation
   431  	switch update.Operation {
   432  	case core.RelationTupleUpdate_CREATE:
   433  		op = v1.RelationshipUpdate_OPERATION_CREATE
   434  	case core.RelationTupleUpdate_DELETE:
   435  		op = v1.RelationshipUpdate_OPERATION_DELETE
   436  	case core.RelationTupleUpdate_TOUCH:
   437  		op = v1.RelationshipUpdate_OPERATION_TOUCH
   438  	default:
   439  		panic("unknown tuple mutation")
   440  	}
   441  
   442  	return &v1.RelationshipUpdate{
   443  		Operation:    op,
   444  		Relationship: ToRelationship(update.Tuple),
   445  	}
   446  }
   447  
   448  // MustFromRelationship converts a Relationship into a RelationTuple.
   449  func MustFromRelationship[R objectReference, S subjectReference[R], C caveat](r relationship[R, S, C]) *core.RelationTuple {
   450  	if err := r.Validate(); err != nil {
   451  		panic(fmt.Sprintf("invalid relationship: %#v %s", r, err))
   452  	}
   453  	return FromRelationship(r)
   454  }
   455  
   456  // MustFromRelationships converts a slice of Relationship's into a slice of RelationTuple's.
   457  func MustFromRelationships[R objectReference, S subjectReference[R], C caveat](rels []relationship[R, S, C]) []*core.RelationTuple {
   458  	tuples := make([]*core.RelationTuple, 0, len(rels))
   459  	for _, rel := range rels {
   460  		tpl := MustFromRelationship(rel)
   461  		tuples = append(tuples, tpl)
   462  	}
   463  	return tuples
   464  }
   465  
   466  // FromRelationship converts a Relationship into a RelationTuple.
   467  func FromRelationship[T objectReference, S subjectReference[T], C caveat](r relationship[T, S, C]) *core.RelationTuple {
   468  	rel := &core.RelationTuple{
   469  		ResourceAndRelation: &core.ObjectAndRelation{},
   470  		Subject:             &core.ObjectAndRelation{},
   471  		Caveat:              &core.ContextualizedCaveat{},
   472  	}
   473  
   474  	CopyRelationshipToRelationTuple(r, rel)
   475  
   476  	return rel
   477  }
   478  
   479  func CopyRelationshipToRelationTuple[T objectReference, S subjectReference[T], C caveat](r relationship[T, S, C], dst *core.RelationTuple) {
   480  	if !reflect.ValueOf(r.GetOptionalCaveat()).IsZero() {
   481  		dst.Caveat.CaveatName = r.GetOptionalCaveat().GetCaveatName()
   482  		dst.Caveat.Context = r.GetOptionalCaveat().GetContext()
   483  	} else {
   484  		dst.Caveat = nil
   485  	}
   486  
   487  	dst.ResourceAndRelation.Namespace = r.GetResource().GetObjectType()
   488  	dst.ResourceAndRelation.ObjectId = r.GetResource().GetObjectId()
   489  	dst.ResourceAndRelation.Relation = r.GetRelation()
   490  	dst.Subject.Namespace = r.GetSubject().GetObject().GetObjectType()
   491  	dst.Subject.ObjectId = r.GetSubject().GetObject().GetObjectId()
   492  	dst.Subject.Relation = stringz.DefaultEmpty(r.GetSubject().GetOptionalRelation(), Ellipsis)
   493  }
   494  
   495  // CopyRelationTupleToRelationship copies a source core.RelationTuple to a
   496  // destination v1.Relationship without allocating new memory. It requires that
   497  // the structure for the destination be pre-allocated for the fixed parts, and
   498  // an optional caveat context be provided for use when the source contains a
   499  // caveat.
   500  func CopyRelationTupleToRelationship(
   501  	src *core.RelationTuple,
   502  	dst *v1.Relationship,
   503  	dstCaveat *v1.ContextualizedCaveat,
   504  ) {
   505  	dst.Resource.ObjectType = src.ResourceAndRelation.Namespace
   506  	dst.Resource.ObjectId = src.ResourceAndRelation.ObjectId
   507  	dst.Relation = src.ResourceAndRelation.Relation
   508  	dst.Subject.Object.ObjectType = src.Subject.Namespace
   509  	dst.Subject.Object.ObjectId = src.Subject.ObjectId
   510  	dst.Subject.OptionalRelation = stringz.Default(src.Subject.Relation, "", Ellipsis)
   511  
   512  	if src.Caveat != nil {
   513  		dst.OptionalCaveat = dstCaveat
   514  		dst.OptionalCaveat.CaveatName = src.Caveat.CaveatName
   515  		dst.OptionalCaveat.Context = src.Caveat.Context
   516  	} else {
   517  		dst.OptionalCaveat = nil
   518  	}
   519  }
   520  
   521  // UpdateFromRelationshipUpdate converts a RelationshipUpdate into a
   522  // RelationTupleUpdate.
   523  func UpdateFromRelationshipUpdate(update *v1.RelationshipUpdate) *core.RelationTupleUpdate {
   524  	var op core.RelationTupleUpdate_Operation
   525  	switch update.Operation {
   526  	case v1.RelationshipUpdate_OPERATION_CREATE:
   527  		op = core.RelationTupleUpdate_CREATE
   528  	case v1.RelationshipUpdate_OPERATION_DELETE:
   529  		op = core.RelationTupleUpdate_DELETE
   530  	case v1.RelationshipUpdate_OPERATION_TOUCH:
   531  		op = core.RelationTupleUpdate_TOUCH
   532  	default:
   533  		panic("unknown tuple mutation")
   534  	}
   535  
   536  	return &core.RelationTupleUpdate{
   537  		Operation: op,
   538  		Tuple:     FromRelationship[*v1.ObjectReference, *v1.SubjectReference, *v1.ContextualizedCaveat](update.Relationship),
   539  	}
   540  }
   541  
   542  // MustWithCaveat adds the given caveat name to the tuple. This is for testing only.
   543  func MustWithCaveat(tpl *core.RelationTuple, caveatName string, contexts ...map[string]any) *core.RelationTuple {
   544  	wc, err := WithCaveat(tpl, caveatName, contexts...)
   545  	if err != nil {
   546  		panic(err)
   547  	}
   548  	return wc
   549  }
   550  
   551  // WithCaveat adds the given caveat name to the tuple. This is for testing only.
   552  func WithCaveat(tpl *core.RelationTuple, caveatName string, contexts ...map[string]any) (*core.RelationTuple, error) {
   553  	var context *structpb.Struct
   554  
   555  	if len(contexts) > 0 {
   556  		combined := map[string]any{}
   557  		for _, current := range contexts {
   558  			maps.Copy(combined, current)
   559  		}
   560  
   561  		contextStruct, err := structpb.NewStruct(combined)
   562  		if err != nil {
   563  			return nil, err
   564  		}
   565  		context = contextStruct
   566  	}
   567  
   568  	tpl = tpl.CloneVT()
   569  	tpl.Caveat = &core.ContextualizedCaveat{
   570  		CaveatName: caveatName,
   571  		Context:    context,
   572  	}
   573  	return tpl, nil
   574  }