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

     1  package v1
     2  
     3  import (
     4  	"sort"
     5  	"strings"
     6  
     7  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
     8  	"golang.org/x/exp/maps"
     9  
    10  	"github.com/authzed/spicedb/pkg/caveats"
    11  	caveattypes "github.com/authzed/spicedb/pkg/caveats/types"
    12  	"github.com/authzed/spicedb/pkg/datastore"
    13  	"github.com/authzed/spicedb/pkg/diff"
    14  	caveatdiff "github.com/authzed/spicedb/pkg/diff/caveats"
    15  	nsdiff "github.com/authzed/spicedb/pkg/diff/namespace"
    16  	"github.com/authzed/spicedb/pkg/namespace"
    17  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    18  	iv1 "github.com/authzed/spicedb/pkg/proto/impl/v1"
    19  	"github.com/authzed/spicedb/pkg/spiceerrors"
    20  	"github.com/authzed/spicedb/pkg/tuple"
    21  	"github.com/authzed/spicedb/pkg/zedtoken"
    22  )
    23  
    24  type schemaFilters struct {
    25  	filters []*v1.ExpSchemaFilter
    26  }
    27  
    28  func newSchemaFilters(filters []*v1.ExpSchemaFilter) (*schemaFilters, error) {
    29  	for _, filter := range filters {
    30  		if filter.OptionalDefinitionNameFilter != "" {
    31  			if filter.OptionalCaveatNameFilter != "" {
    32  				return nil, NewInvalidFilterErr("cannot filter by both definition and caveat name", filter.String())
    33  			}
    34  		}
    35  
    36  		if filter.OptionalRelationNameFilter != "" {
    37  			if filter.OptionalDefinitionNameFilter == "" {
    38  				return nil, NewInvalidFilterErr("relation name match requires definition name match", filter.String())
    39  			}
    40  
    41  			if filter.OptionalPermissionNameFilter != "" {
    42  				return nil, NewInvalidFilterErr("cannot filter by both relation and permission name", filter.String())
    43  			}
    44  		}
    45  
    46  		if filter.OptionalPermissionNameFilter != "" {
    47  			if filter.OptionalDefinitionNameFilter == "" {
    48  				return nil, NewInvalidFilterErr("permission name match requires definition name match", filter.String())
    49  			}
    50  		}
    51  	}
    52  
    53  	return &schemaFilters{filters: filters}, nil
    54  }
    55  
    56  func (sf *schemaFilters) HasNamespaces() bool {
    57  	if len(sf.filters) == 0 {
    58  		return true
    59  	}
    60  
    61  	for _, filter := range sf.filters {
    62  		if filter.OptionalDefinitionNameFilter != "" {
    63  			return true
    64  		}
    65  	}
    66  
    67  	return false
    68  }
    69  
    70  func (sf *schemaFilters) HasCaveats() bool {
    71  	if len(sf.filters) == 0 {
    72  		return true
    73  	}
    74  
    75  	for _, filter := range sf.filters {
    76  		if filter.OptionalCaveatNameFilter != "" {
    77  			return true
    78  		}
    79  	}
    80  
    81  	return false
    82  }
    83  
    84  func (sf *schemaFilters) HasNamespace(namespaceName string) bool {
    85  	if len(sf.filters) == 0 {
    86  		return true
    87  	}
    88  
    89  	hasDefinitionFilter := false
    90  	for _, filter := range sf.filters {
    91  		if filter.OptionalDefinitionNameFilter == "" {
    92  			continue
    93  		}
    94  
    95  		hasDefinitionFilter = true
    96  		isMatch := strings.HasPrefix(namespaceName, filter.OptionalDefinitionNameFilter)
    97  		if isMatch {
    98  			return true
    99  		}
   100  	}
   101  
   102  	return !hasDefinitionFilter
   103  }
   104  
   105  func (sf *schemaFilters) HasCaveat(caveatName string) bool {
   106  	if len(sf.filters) == 0 {
   107  		return true
   108  	}
   109  
   110  	hasCaveatFilter := false
   111  	for _, filter := range sf.filters {
   112  		if filter.OptionalCaveatNameFilter == "" {
   113  			continue
   114  		}
   115  
   116  		hasCaveatFilter = true
   117  		isMatch := strings.HasPrefix(caveatName, filter.OptionalCaveatNameFilter)
   118  		if isMatch {
   119  			return true
   120  		}
   121  	}
   122  
   123  	return !hasCaveatFilter
   124  }
   125  
   126  func (sf *schemaFilters) HasRelation(namespaceName, relationName string) bool {
   127  	if len(sf.filters) == 0 {
   128  		return true
   129  	}
   130  
   131  	hasRelationFilter := false
   132  	for _, filter := range sf.filters {
   133  		if filter.OptionalRelationNameFilter == "" {
   134  			continue
   135  		}
   136  
   137  		hasRelationFilter = true
   138  		isMatch := strings.HasPrefix(relationName, filter.OptionalRelationNameFilter)
   139  		if !isMatch {
   140  			continue
   141  		}
   142  
   143  		isMatch = strings.HasPrefix(namespaceName, filter.OptionalDefinitionNameFilter)
   144  		if isMatch {
   145  			return true
   146  		}
   147  	}
   148  
   149  	return !hasRelationFilter
   150  }
   151  
   152  func (sf *schemaFilters) HasPermission(namespaceName, permissionName string) bool {
   153  	if len(sf.filters) == 0 {
   154  		return true
   155  	}
   156  
   157  	hasPermissionFilter := false
   158  	for _, filter := range sf.filters {
   159  		if filter.OptionalPermissionNameFilter == "" {
   160  			continue
   161  		}
   162  
   163  		hasPermissionFilter = true
   164  		isMatch := strings.HasPrefix(permissionName, filter.OptionalPermissionNameFilter)
   165  		if !isMatch {
   166  			continue
   167  		}
   168  
   169  		isMatch = strings.HasPrefix(namespaceName, filter.OptionalDefinitionNameFilter)
   170  		if isMatch {
   171  			return true
   172  		}
   173  	}
   174  
   175  	return !hasPermissionFilter
   176  }
   177  
   178  // convertDiff converts a schema diff into an API response.
   179  func convertDiff(
   180  	diff *diff.SchemaDiff,
   181  	existingSchema *diff.DiffableSchema,
   182  	comparisonSchema *diff.DiffableSchema,
   183  	atRevision datastore.Revision,
   184  ) (*v1.ExperimentalDiffSchemaResponse, error) {
   185  	size := len(diff.AddedNamespaces) + len(diff.RemovedNamespaces) + len(diff.AddedCaveats) + len(diff.RemovedCaveats) + len(diff.ChangedNamespaces) + len(diff.ChangedCaveats)
   186  	diffs := make([]*v1.ExpSchemaDiff, 0, size)
   187  
   188  	// Add/remove namespaces.
   189  	for _, ns := range diff.AddedNamespaces {
   190  		nsDef, err := namespaceAPIReprForName(ns, comparisonSchema)
   191  		if err != nil {
   192  			return nil, err
   193  		}
   194  
   195  		diffs = append(diffs, &v1.ExpSchemaDiff{
   196  			Diff: &v1.ExpSchemaDiff_DefinitionAdded{
   197  				DefinitionAdded: nsDef,
   198  			},
   199  		})
   200  	}
   201  
   202  	for _, ns := range diff.RemovedNamespaces {
   203  		nsDef, err := namespaceAPIReprForName(ns, existingSchema)
   204  		if err != nil {
   205  			return nil, err
   206  		}
   207  
   208  		diffs = append(diffs, &v1.ExpSchemaDiff{
   209  			Diff: &v1.ExpSchemaDiff_DefinitionRemoved{
   210  				DefinitionRemoved: nsDef,
   211  			},
   212  		})
   213  	}
   214  
   215  	// Add/remove caveats.
   216  	for _, caveat := range diff.AddedCaveats {
   217  		caveatDef, err := caveatAPIReprForName(caveat, comparisonSchema)
   218  		if err != nil {
   219  			return nil, err
   220  		}
   221  
   222  		diffs = append(diffs, &v1.ExpSchemaDiff{
   223  			Diff: &v1.ExpSchemaDiff_CaveatAdded{
   224  				CaveatAdded: caveatDef,
   225  			},
   226  		})
   227  	}
   228  
   229  	for _, caveat := range diff.RemovedCaveats {
   230  		caveatDef, err := caveatAPIReprForName(caveat, existingSchema)
   231  		if err != nil {
   232  			return nil, err
   233  		}
   234  
   235  		diffs = append(diffs, &v1.ExpSchemaDiff{
   236  			Diff: &v1.ExpSchemaDiff_CaveatRemoved{
   237  				CaveatRemoved: caveatDef,
   238  			},
   239  		})
   240  	}
   241  
   242  	// Changed namespaces.
   243  	for nsName, nsDiff := range diff.ChangedNamespaces {
   244  		for _, delta := range nsDiff.Deltas() {
   245  			switch delta.Type {
   246  			case nsdiff.AddedPermission:
   247  				permission, ok := comparisonSchema.GetRelation(nsName, delta.RelationName)
   248  				if !ok {
   249  					return nil, spiceerrors.MustBugf("permission %q not found in namespace %q", delta.RelationName, nsName)
   250  				}
   251  
   252  				perm, err := permissionAPIRepr(permission, nsName, nil)
   253  				if err != nil {
   254  					return nil, err
   255  				}
   256  
   257  				diffs = append(diffs, &v1.ExpSchemaDiff{
   258  					Diff: &v1.ExpSchemaDiff_PermissionAdded{
   259  						PermissionAdded: perm,
   260  					},
   261  				})
   262  
   263  			case nsdiff.AddedRelation:
   264  				relation, ok := comparisonSchema.GetRelation(nsName, delta.RelationName)
   265  				if !ok {
   266  					return nil, spiceerrors.MustBugf("relation %q not found in namespace %q", delta.RelationName, nsName)
   267  				}
   268  
   269  				rel, err := relationAPIRepr(relation, nsName, nil)
   270  				if err != nil {
   271  					return nil, err
   272  				}
   273  
   274  				diffs = append(diffs, &v1.ExpSchemaDiff{
   275  					Diff: &v1.ExpSchemaDiff_RelationAdded{
   276  						RelationAdded: rel,
   277  					},
   278  				})
   279  
   280  			case nsdiff.ChangedPermissionComment:
   281  				permission, ok := comparisonSchema.GetRelation(nsName, delta.RelationName)
   282  				if !ok {
   283  					return nil, spiceerrors.MustBugf("permission %q not found in namespace %q", delta.RelationName, nsName)
   284  				}
   285  
   286  				perm, err := permissionAPIRepr(permission, nsName, nil)
   287  				if err != nil {
   288  					return nil, err
   289  				}
   290  
   291  				diffs = append(diffs, &v1.ExpSchemaDiff{
   292  					Diff: &v1.ExpSchemaDiff_PermissionDocCommentChanged{
   293  						PermissionDocCommentChanged: perm,
   294  					},
   295  				})
   296  
   297  			case nsdiff.ChangedPermissionImpl:
   298  				permission, ok := comparisonSchema.GetRelation(nsName, delta.RelationName)
   299  				if !ok {
   300  					return nil, spiceerrors.MustBugf("permission %q not found in namespace %q", delta.RelationName, nsName)
   301  				}
   302  
   303  				perm, err := permissionAPIRepr(permission, nsName, nil)
   304  				if err != nil {
   305  					return nil, err
   306  				}
   307  
   308  				diffs = append(diffs, &v1.ExpSchemaDiff{
   309  					Diff: &v1.ExpSchemaDiff_PermissionExprChanged{
   310  						PermissionExprChanged: perm,
   311  					},
   312  				})
   313  
   314  			case nsdiff.ChangedRelationComment:
   315  				relation, ok := comparisonSchema.GetRelation(nsName, delta.RelationName)
   316  				if !ok {
   317  					return nil, spiceerrors.MustBugf("relation %q not found in namespace %q", delta.RelationName, nsName)
   318  				}
   319  
   320  				rel, err := relationAPIRepr(relation, nsName, nil)
   321  				if err != nil {
   322  					return nil, err
   323  				}
   324  
   325  				diffs = append(diffs, &v1.ExpSchemaDiff{
   326  					Diff: &v1.ExpSchemaDiff_RelationDocCommentChanged{
   327  						RelationDocCommentChanged: rel,
   328  					},
   329  				})
   330  
   331  			case nsdiff.LegacyChangedRelationImpl:
   332  				return nil, spiceerrors.MustBugf("legacy relation implementation changes are not supported")
   333  
   334  			case nsdiff.NamespaceCommentsChanged:
   335  				def, err := namespaceAPIReprForName(nsName, comparisonSchema)
   336  				if err != nil {
   337  					return nil, err
   338  				}
   339  
   340  				diffs = append(diffs, &v1.ExpSchemaDiff{
   341  					Diff: &v1.ExpSchemaDiff_DefinitionDocCommentChanged{
   342  						DefinitionDocCommentChanged: def,
   343  					},
   344  				})
   345  
   346  			case nsdiff.RelationAllowedTypeRemoved:
   347  				relation, ok := comparisonSchema.GetRelation(nsName, delta.RelationName)
   348  				if !ok {
   349  					return nil, spiceerrors.MustBugf("relation %q not found in namespace %q", delta.RelationName, nsName)
   350  				}
   351  
   352  				rel, err := relationAPIRepr(relation, nsName, nil)
   353  				if err != nil {
   354  					return nil, err
   355  				}
   356  
   357  				diffs = append(diffs, &v1.ExpSchemaDiff{
   358  					Diff: &v1.ExpSchemaDiff_RelationSubjectTypeRemoved{
   359  						RelationSubjectTypeRemoved: &v1.ExpRelationSubjectTypeChange{
   360  							Relation:           rel,
   361  							ChangedSubjectType: typeAPIRepr(delta.AllowedType),
   362  						},
   363  					},
   364  				})
   365  
   366  			case nsdiff.RelationAllowedTypeAdded:
   367  				relation, ok := comparisonSchema.GetRelation(nsName, delta.RelationName)
   368  				if !ok {
   369  					return nil, spiceerrors.MustBugf("relation %q not found in namespace %q", delta.RelationName, nsName)
   370  				}
   371  
   372  				rel, err := relationAPIRepr(relation, nsName, nil)
   373  				if err != nil {
   374  					return nil, err
   375  				}
   376  
   377  				diffs = append(diffs, &v1.ExpSchemaDiff{
   378  					Diff: &v1.ExpSchemaDiff_RelationSubjectTypeAdded{
   379  						RelationSubjectTypeAdded: &v1.ExpRelationSubjectTypeChange{
   380  							Relation:           rel,
   381  							ChangedSubjectType: typeAPIRepr(delta.AllowedType),
   382  						},
   383  					},
   384  				})
   385  
   386  			case nsdiff.RemovedPermission:
   387  				permission, ok := existingSchema.GetRelation(nsName, delta.RelationName)
   388  				if !ok {
   389  					return nil, spiceerrors.MustBugf("relation %q not found in namespace %q", delta.RelationName, nsName)
   390  				}
   391  
   392  				perm, err := permissionAPIRepr(permission, nsName, nil)
   393  				if err != nil {
   394  					return nil, err
   395  				}
   396  
   397  				diffs = append(diffs, &v1.ExpSchemaDiff{
   398  					Diff: &v1.ExpSchemaDiff_PermissionRemoved{
   399  						PermissionRemoved: perm,
   400  					},
   401  				})
   402  
   403  			case nsdiff.RemovedRelation:
   404  				relation, ok := existingSchema.GetRelation(nsName, delta.RelationName)
   405  				if !ok {
   406  					return nil, spiceerrors.MustBugf("relation %q not found in namespace %q", delta.RelationName, nsName)
   407  				}
   408  
   409  				rel, err := relationAPIRepr(relation, nsName, nil)
   410  				if err != nil {
   411  					return nil, err
   412  				}
   413  
   414  				diffs = append(diffs, &v1.ExpSchemaDiff{
   415  					Diff: &v1.ExpSchemaDiff_RelationRemoved{
   416  						RelationRemoved: rel,
   417  					},
   418  				})
   419  
   420  			case nsdiff.NamespaceAdded:
   421  				return nil, spiceerrors.MustBugf("should be handled above")
   422  
   423  			case nsdiff.NamespaceRemoved:
   424  				return nil, spiceerrors.MustBugf("should be handled above")
   425  
   426  			default:
   427  				return nil, spiceerrors.MustBugf("unexpected delta type %v", delta.Type)
   428  			}
   429  		}
   430  	}
   431  
   432  	// Changed caveats.
   433  	for caveatName, caveatDiff := range diff.ChangedCaveats {
   434  		for _, delta := range caveatDiff.Deltas() {
   435  			switch delta.Type {
   436  			case caveatdiff.CaveatCommentsChanged:
   437  				caveat, err := caveatAPIReprForName(caveatName, comparisonSchema)
   438  				if err != nil {
   439  					return nil, err
   440  				}
   441  
   442  				diffs = append(diffs, &v1.ExpSchemaDiff{
   443  					Diff: &v1.ExpSchemaDiff_CaveatDocCommentChanged{
   444  						CaveatDocCommentChanged: caveat,
   445  					},
   446  				})
   447  
   448  			case caveatdiff.AddedParameter:
   449  				paramDef, err := caveatAPIParamRepr(delta.ParameterName, caveatName, comparisonSchema)
   450  				if err != nil {
   451  					return nil, err
   452  				}
   453  
   454  				diffs = append(diffs, &v1.ExpSchemaDiff{
   455  					Diff: &v1.ExpSchemaDiff_CaveatParameterAdded{
   456  						CaveatParameterAdded: paramDef,
   457  					},
   458  				})
   459  
   460  			case caveatdiff.RemovedParameter:
   461  				paramDef, err := caveatAPIParamRepr(delta.ParameterName, caveatName, existingSchema)
   462  				if err != nil {
   463  					return nil, err
   464  				}
   465  
   466  				diffs = append(diffs, &v1.ExpSchemaDiff{
   467  					Diff: &v1.ExpSchemaDiff_CaveatParameterRemoved{
   468  						CaveatParameterRemoved: paramDef,
   469  					},
   470  				})
   471  
   472  			case caveatdiff.ParameterTypeChanged:
   473  				previousParamDef, err := caveatAPIParamRepr(delta.ParameterName, caveatName, existingSchema)
   474  				if err != nil {
   475  					return nil, err
   476  				}
   477  
   478  				paramDef, err := caveatAPIParamRepr(delta.ParameterName, caveatName, comparisonSchema)
   479  				if err != nil {
   480  					return nil, err
   481  				}
   482  
   483  				diffs = append(diffs, &v1.ExpSchemaDiff{
   484  					Diff: &v1.ExpSchemaDiff_CaveatParameterTypeChanged{
   485  						CaveatParameterTypeChanged: &v1.ExpCaveatParameterTypeChange{
   486  							Parameter:    paramDef,
   487  							PreviousType: previousParamDef.Type,
   488  						},
   489  					},
   490  				})
   491  
   492  			case caveatdiff.CaveatExpressionChanged:
   493  				caveat, err := caveatAPIReprForName(caveatName, comparisonSchema)
   494  				if err != nil {
   495  					return nil, err
   496  				}
   497  
   498  				diffs = append(diffs, &v1.ExpSchemaDiff{
   499  					Diff: &v1.ExpSchemaDiff_CaveatExprChanged{
   500  						CaveatExprChanged: caveat,
   501  					},
   502  				})
   503  
   504  			case caveatdiff.CaveatAdded:
   505  				return nil, spiceerrors.MustBugf("should be handled above")
   506  
   507  			case caveatdiff.CaveatRemoved:
   508  				return nil, spiceerrors.MustBugf("should be handled above")
   509  
   510  			default:
   511  				return nil, spiceerrors.MustBugf("unexpected delta type %v", delta.Type)
   512  			}
   513  		}
   514  	}
   515  
   516  	return &v1.ExperimentalDiffSchemaResponse{
   517  		Diffs:  diffs,
   518  		ReadAt: zedtoken.MustNewFromRevision(atRevision),
   519  	}, nil
   520  }
   521  
   522  // namespaceAPIReprForName builds an API representation of a namespace.
   523  func namespaceAPIReprForName(namespaceName string, schema *diff.DiffableSchema) (*v1.ExpDefinition, error) {
   524  	nsDef, ok := schema.GetNamespace(namespaceName)
   525  	if !ok {
   526  		return nil, spiceerrors.MustBugf("namespace %q not found in schema", namespaceName)
   527  	}
   528  
   529  	return namespaceAPIRepr(nsDef, nil)
   530  }
   531  
   532  func namespaceAPIRepr(nsDef *core.NamespaceDefinition, schemaFilters *schemaFilters) (*v1.ExpDefinition, error) {
   533  	if schemaFilters != nil && !schemaFilters.HasNamespace(nsDef.Name) {
   534  		return nil, nil
   535  	}
   536  
   537  	relations := make([]*v1.ExpRelation, 0, len(nsDef.Relation))
   538  	permissions := make([]*v1.ExpPermission, 0, len(nsDef.Relation))
   539  
   540  	for _, rel := range nsDef.Relation {
   541  		if namespace.GetRelationKind(rel) == iv1.RelationMetadata_PERMISSION {
   542  			permission, err := permissionAPIRepr(rel, nsDef.Name, schemaFilters)
   543  			if err != nil {
   544  				return nil, err
   545  			}
   546  
   547  			if permission != nil {
   548  				permissions = append(permissions, permission)
   549  			}
   550  			continue
   551  		}
   552  
   553  		relation, err := relationAPIRepr(rel, nsDef.Name, schemaFilters)
   554  		if err != nil {
   555  			return nil, err
   556  		}
   557  
   558  		if relation != nil {
   559  			relations = append(relations, relation)
   560  		}
   561  	}
   562  
   563  	comments := namespace.GetComments(nsDef.Metadata)
   564  	return &v1.ExpDefinition{
   565  		Name:        nsDef.Name,
   566  		Comment:     strings.Join(comments, "\n"),
   567  		Relations:   relations,
   568  		Permissions: permissions,
   569  	}, nil
   570  }
   571  
   572  // permissionAPIRepr builds an API representation of a permission.
   573  func permissionAPIRepr(relation *core.Relation, parentDefName string, schemaFilters *schemaFilters) (*v1.ExpPermission, error) {
   574  	if schemaFilters != nil && !schemaFilters.HasPermission(parentDefName, relation.Name) {
   575  		return nil, nil
   576  	}
   577  
   578  	comments := namespace.GetComments(relation.Metadata)
   579  	return &v1.ExpPermission{
   580  		Name:                 relation.Name,
   581  		Comment:              strings.Join(comments, "\n"),
   582  		ParentDefinitionName: parentDefName,
   583  	}, nil
   584  }
   585  
   586  // relationAPIRepresentation builds an API representation of a relation.
   587  func relationAPIRepr(relation *core.Relation, parentDefName string, schemaFilters *schemaFilters) (*v1.ExpRelation, error) {
   588  	if schemaFilters != nil && !schemaFilters.HasRelation(parentDefName, relation.Name) {
   589  		return nil, nil
   590  	}
   591  
   592  	comments := namespace.GetComments(relation.Metadata)
   593  
   594  	var subjectTypes []*v1.ExpTypeReference
   595  	if relation.TypeInformation != nil {
   596  		subjectTypes = make([]*v1.ExpTypeReference, 0, len(relation.TypeInformation.AllowedDirectRelations))
   597  		for _, subjectType := range relation.TypeInformation.AllowedDirectRelations {
   598  			typeref := typeAPIRepr(subjectType)
   599  			subjectTypes = append(subjectTypes, typeref)
   600  		}
   601  	}
   602  
   603  	return &v1.ExpRelation{
   604  		Name:                 relation.Name,
   605  		Comment:              strings.Join(comments, "\n"),
   606  		ParentDefinitionName: parentDefName,
   607  		SubjectTypes:         subjectTypes,
   608  	}, nil
   609  }
   610  
   611  // typeAPIRepr builds an API representation of a type.
   612  func typeAPIRepr(subjectType *core.AllowedRelation) *v1.ExpTypeReference {
   613  	typeref := &v1.ExpTypeReference{
   614  		SubjectDefinitionName: subjectType.Namespace,
   615  		Typeref:               &v1.ExpTypeReference_IsTerminalSubject{},
   616  	}
   617  
   618  	if subjectType.GetRelation() != tuple.Ellipsis && subjectType.GetRelation() != "" {
   619  		typeref.Typeref = &v1.ExpTypeReference_OptionalRelationName{
   620  			OptionalRelationName: subjectType.GetRelation(),
   621  		}
   622  	} else if subjectType.GetPublicWildcard() != nil {
   623  		typeref.Typeref = &v1.ExpTypeReference_IsPublicWildcard{
   624  			IsPublicWildcard: true,
   625  		}
   626  	}
   627  
   628  	if subjectType.GetRequiredCaveat() != nil {
   629  		typeref.OptionalCaveatName = subjectType.GetRequiredCaveat().CaveatName
   630  	}
   631  
   632  	return typeref
   633  }
   634  
   635  // caveatAPIReprForName builds an API representation of a caveat.
   636  func caveatAPIReprForName(caveatName string, schema *diff.DiffableSchema) (*v1.ExpCaveat, error) {
   637  	caveatDef, ok := schema.GetCaveat(caveatName)
   638  	if !ok {
   639  		return nil, spiceerrors.MustBugf("caveat %q not found in schema", caveatName)
   640  	}
   641  
   642  	return caveatAPIRepr(caveatDef, nil)
   643  }
   644  
   645  // caveatAPIRepr builds an API representation of a caveat.
   646  func caveatAPIRepr(caveatDef *core.CaveatDefinition, schemaFilters *schemaFilters) (*v1.ExpCaveat, error) {
   647  	if schemaFilters != nil && !schemaFilters.HasCaveat(caveatDef.Name) {
   648  		return nil, nil
   649  	}
   650  
   651  	parameters := make([]*v1.ExpCaveatParameter, 0, len(caveatDef.ParameterTypes))
   652  	paramNames := maps.Keys(caveatDef.ParameterTypes)
   653  	sort.Strings(paramNames)
   654  
   655  	for _, paramName := range paramNames {
   656  		paramType, ok := caveatDef.ParameterTypes[paramName]
   657  		if !ok {
   658  			return nil, spiceerrors.MustBugf("parameter %q not found in caveat %q", paramName, caveatDef.Name)
   659  		}
   660  
   661  		decoded, err := caveattypes.DecodeParameterType(paramType)
   662  		if err != nil {
   663  			return nil, spiceerrors.MustBugf("invalid parameter type on caveat: %v", err)
   664  		}
   665  
   666  		parameters = append(parameters, &v1.ExpCaveatParameter{
   667  			Name:             paramName,
   668  			Type:             decoded.String(),
   669  			ParentCaveatName: caveatDef.Name,
   670  		})
   671  	}
   672  
   673  	parameterTypes, err := caveattypes.DecodeParameterTypes(caveatDef.ParameterTypes)
   674  	if err != nil {
   675  		return nil, spiceerrors.MustBugf("invalid caveat parameters: %v", err)
   676  	}
   677  
   678  	deserializedExpression, err := caveats.DeserializeCaveat(caveatDef.SerializedExpression, parameterTypes)
   679  	if err != nil {
   680  		return nil, spiceerrors.MustBugf("invalid caveat expression bytes: %v", err)
   681  	}
   682  
   683  	exprString, err := deserializedExpression.ExprString()
   684  	if err != nil {
   685  		return nil, spiceerrors.MustBugf("invalid caveat expression: %v", err)
   686  	}
   687  
   688  	comments := namespace.GetComments(caveatDef.Metadata)
   689  	return &v1.ExpCaveat{
   690  		Name:       caveatDef.Name,
   691  		Comment:    strings.Join(comments, "\n"),
   692  		Parameters: parameters,
   693  		Expression: exprString,
   694  	}, nil
   695  }
   696  
   697  // caveatAPIParamRepresentation builds an API representation of a caveat parameter.
   698  func caveatAPIParamRepr(paramName, parentCaveatName string, schema *diff.DiffableSchema) (*v1.ExpCaveatParameter, error) {
   699  	caveatDef, ok := schema.GetCaveat(parentCaveatName)
   700  	if !ok {
   701  		return nil, spiceerrors.MustBugf("caveat %q not found in schema", parentCaveatName)
   702  	}
   703  
   704  	paramType, ok := caveatDef.ParameterTypes[paramName]
   705  	if !ok {
   706  		return nil, spiceerrors.MustBugf("parameter %q not found in caveat %q", paramName, parentCaveatName)
   707  	}
   708  
   709  	decoded, err := caveattypes.DecodeParameterType(paramType)
   710  	if err != nil {
   711  		return nil, spiceerrors.MustBugf("invalid parameter type on caveat: %v", err)
   712  	}
   713  
   714  	return &v1.ExpCaveatParameter{
   715  		Name:             paramName,
   716  		Type:             decoded.String(),
   717  		ParentCaveatName: parentCaveatName,
   718  	}, nil
   719  }