github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/types/resource.go (about)

     1  /*
     2  Copyright 2020 Gravitational, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package types
    18  
    19  import (
    20  	"regexp"
    21  	"slices"
    22  	"sort"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/gravitational/trace"
    27  
    28  	"github.com/gravitational/teleport/api/defaults"
    29  	"github.com/gravitational/teleport/api/types/common"
    30  	"github.com/gravitational/teleport/api/types/compare"
    31  	"github.com/gravitational/teleport/api/utils"
    32  )
    33  
    34  var (
    35  	_ compare.IsEqual[*ResourceHeader] = (*ResourceHeader)(nil)
    36  	_ compare.IsEqual[*Metadata]       = (*Metadata)(nil)
    37  )
    38  
    39  // Resource represents common properties for all resources.
    40  //
    41  // Please avoid adding new uses of Resource in the codebase. Instead, consider
    42  // using concrete proto types directly or a manually declared subset of the
    43  // Resource153 interface for new-style resources.
    44  type Resource interface {
    45  	// GetKind returns resource kind
    46  	GetKind() string
    47  	// GetSubKind returns resource subkind
    48  	GetSubKind() string
    49  	// SetSubKind sets resource subkind
    50  	SetSubKind(string)
    51  	// GetVersion returns resource version
    52  	GetVersion() string
    53  	// GetName returns the name of the resource
    54  	GetName() string
    55  	// SetName sets the name of the resource
    56  	SetName(string)
    57  	// Expiry returns object expiry setting
    58  	Expiry() time.Time
    59  	// SetExpiry sets object expiry
    60  	SetExpiry(time.Time)
    61  	// GetMetadata returns object metadata
    62  	GetMetadata() Metadata
    63  	// GetResourceID returns resource ID
    64  	// Deprecated: use GetRevision instead
    65  	GetResourceID() int64
    66  	// SetResourceID sets resource ID
    67  	// Deprecated: use SetRevision instead
    68  	SetResourceID(int64)
    69  	// GetRevision returns the revision
    70  	GetRevision() string
    71  	// SetRevision sets the revision
    72  	SetRevision(string)
    73  }
    74  
    75  // IsSystemResource checks to see if the given resource is considered
    76  // part of the teleport system, as opposed to some user created resource
    77  // or preset.
    78  func IsSystemResource(r Resource) bool {
    79  	metadata := r.GetMetadata()
    80  	if t, ok := metadata.Labels[TeleportInternalResourceType]; ok {
    81  		return t == SystemResource
    82  	}
    83  	return false
    84  }
    85  
    86  // GetName fetches the name of the supplied resource. Useful when sorting lists
    87  // of resources or building maps, etc.
    88  func GetName[R Resource](r R) string {
    89  	return r.GetName()
    90  }
    91  
    92  // ResourceDetails includes details about the resource
    93  type ResourceDetails struct {
    94  	Hostname     string
    95  	FriendlyName string
    96  }
    97  
    98  // ResourceWithSecrets includes additional properties which must
    99  // be provided by resources which *may* contain secrets.
   100  type ResourceWithSecrets interface {
   101  	Resource
   102  	// WithoutSecrets returns an instance of the resource which
   103  	// has had all secrets removed.  If the current resource has
   104  	// already had its secrets removed, this may be a no-op.
   105  	WithoutSecrets() Resource
   106  }
   107  
   108  // ResourceWithOrigin provides information on the origin of the resource
   109  // (defaults, config-file, dynamic).
   110  type ResourceWithOrigin interface {
   111  	Resource
   112  	// Origin returns the origin value of the resource.
   113  	Origin() string
   114  	// SetOrigin sets the origin value of the resource.
   115  	SetOrigin(string)
   116  }
   117  
   118  // ResourceWithLabels is a common interface for resources that have labels.
   119  type ResourceWithLabels interface {
   120  	// ResourceWithOrigin is the base resource interface.
   121  	ResourceWithOrigin
   122  	// GetLabel retrieves the label with the provided key.
   123  	GetLabel(key string) (value string, ok bool)
   124  	// GetAllLabels returns all resource's labels.
   125  	GetAllLabels() map[string]string
   126  	// GetStaticLabels returns the resource's static labels.
   127  	GetStaticLabels() map[string]string
   128  	// SetStaticLabels sets the resource's static labels.
   129  	SetStaticLabels(sl map[string]string)
   130  	// MatchSearch goes through select field values of a resource
   131  	// and tries to match against the list of search values.
   132  	MatchSearch(searchValues []string) bool
   133  }
   134  
   135  // EnrichedResource is a [ResourceWithLabels] wrapped with
   136  // additional user-specific information.
   137  type EnrichedResource struct {
   138  	// ResourceWithLabels is the underlying resource.
   139  	ResourceWithLabels
   140  	// Logins that the user is allowed to access the above resource with.
   141  	Logins []string
   142  	// RequiresRequest is true if a resource is being returned to the user but requires
   143  	// an access request to access. This is done during `ListUnifiedResources` when
   144  	// searchAsRoles is true
   145  	RequiresRequest bool
   146  }
   147  
   148  // ResourcesWithLabels is a list of labeled resources.
   149  type ResourcesWithLabels []ResourceWithLabels
   150  
   151  // ResourcesWithLabelsMap is like ResourcesWithLabels, but a map from resource name to its value.
   152  type ResourcesWithLabelsMap map[string]ResourceWithLabels
   153  
   154  // ToMap returns these databases as a map keyed by database name.
   155  func (r ResourcesWithLabels) ToMap() ResourcesWithLabelsMap {
   156  	rm := make(ResourcesWithLabelsMap, len(r))
   157  
   158  	// there may be duplicate resources in the input list.
   159  	// by iterating from end to start, the first resource of given name wins.
   160  	for i := len(r) - 1; i >= 0; i-- {
   161  		resource := r[i]
   162  		rm[resource.GetName()] = resource
   163  	}
   164  
   165  	return rm
   166  }
   167  
   168  // Len returns the slice length.
   169  func (r ResourcesWithLabels) Len() int { return len(r) }
   170  
   171  // Less compares resources by name.
   172  func (r ResourcesWithLabels) Less(i, j int) bool { return r[i].GetName() < r[j].GetName() }
   173  
   174  // Swap swaps two resources.
   175  func (r ResourcesWithLabels) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
   176  
   177  // AsAppServers converts each resource into type AppServer.
   178  func (r ResourcesWithLabels) AsAppServers() ([]AppServer, error) {
   179  	apps := make([]AppServer, 0, len(r))
   180  	for _, resource := range r {
   181  		app, ok := resource.(AppServer)
   182  		if !ok {
   183  			return nil, trace.BadParameter("expected types.AppServer, got: %T", resource)
   184  		}
   185  		apps = append(apps, app)
   186  	}
   187  	return apps, nil
   188  }
   189  
   190  // AsServers converts each resource into type Server.
   191  func (r ResourcesWithLabels) AsServers() ([]Server, error) {
   192  	servers := make([]Server, 0, len(r))
   193  	for _, resource := range r {
   194  		server, ok := resource.(Server)
   195  		if !ok {
   196  			return nil, trace.BadParameter("expected types.Server, got: %T", resource)
   197  		}
   198  		servers = append(servers, server)
   199  	}
   200  	return servers, nil
   201  }
   202  
   203  // AsDatabases converts each resource into type Database.
   204  func (r ResourcesWithLabels) AsDatabases() ([]Database, error) {
   205  	dbs := make([]Database, 0, len(r))
   206  	for _, resource := range r {
   207  		db, ok := resource.(Database)
   208  		if !ok {
   209  			return nil, trace.BadParameter("expected types.Database, got: %T", resource)
   210  		}
   211  		dbs = append(dbs, db)
   212  	}
   213  	return dbs, nil
   214  }
   215  
   216  // AsDatabaseServers converts each resource into type DatabaseServer.
   217  func (r ResourcesWithLabels) AsDatabaseServers() ([]DatabaseServer, error) {
   218  	dbs := make([]DatabaseServer, 0, len(r))
   219  	for _, resource := range r {
   220  		db, ok := resource.(DatabaseServer)
   221  		if !ok {
   222  			return nil, trace.BadParameter("expected types.DatabaseServer, got: %T", resource)
   223  		}
   224  		dbs = append(dbs, db)
   225  	}
   226  	return dbs, nil
   227  }
   228  
   229  // AsDatabaseServices converts each resource into type DatabaseService.
   230  func (r ResourcesWithLabels) AsDatabaseServices() ([]DatabaseService, error) {
   231  	services := make([]DatabaseService, len(r))
   232  	for i, resource := range r {
   233  		dbService, ok := resource.(DatabaseService)
   234  		if !ok {
   235  			return nil, trace.BadParameter("expected types.DatabaseService, got: %T", resource)
   236  		}
   237  		services[i] = dbService
   238  	}
   239  	return services, nil
   240  }
   241  
   242  // AsWindowsDesktops converts each resource into type WindowsDesktop.
   243  func (r ResourcesWithLabels) AsWindowsDesktops() ([]WindowsDesktop, error) {
   244  	desktops := make([]WindowsDesktop, 0, len(r))
   245  	for _, resource := range r {
   246  		desktop, ok := resource.(WindowsDesktop)
   247  		if !ok {
   248  			return nil, trace.BadParameter("expected types.WindowsDesktop, got: %T", resource)
   249  		}
   250  		desktops = append(desktops, desktop)
   251  	}
   252  	return desktops, nil
   253  }
   254  
   255  // AsWindowsDesktopServices converts each resource into type WindowsDesktop.
   256  func (r ResourcesWithLabels) AsWindowsDesktopServices() ([]WindowsDesktopService, error) {
   257  	desktopServices := make([]WindowsDesktopService, 0, len(r))
   258  	for _, resource := range r {
   259  		desktopService, ok := resource.(WindowsDesktopService)
   260  		if !ok {
   261  			return nil, trace.BadParameter("expected types.WindowsDesktopService, got: %T", resource)
   262  		}
   263  		desktopServices = append(desktopServices, desktopService)
   264  	}
   265  	return desktopServices, nil
   266  }
   267  
   268  // AsKubeClusters converts each resource into type KubeCluster.
   269  func (r ResourcesWithLabels) AsKubeClusters() ([]KubeCluster, error) {
   270  	clusters := make([]KubeCluster, 0, len(r))
   271  	for _, resource := range r {
   272  		cluster, ok := resource.(KubeCluster)
   273  		if !ok {
   274  			return nil, trace.BadParameter("expected types.KubeCluster, got: %T", resource)
   275  		}
   276  		clusters = append(clusters, cluster)
   277  	}
   278  	return clusters, nil
   279  }
   280  
   281  // AsKubeServers converts each resource into type KubeServer.
   282  func (r ResourcesWithLabels) AsKubeServers() ([]KubeServer, error) {
   283  	servers := make([]KubeServer, 0, len(r))
   284  	for _, resource := range r {
   285  		server, ok := resource.(KubeServer)
   286  		if !ok {
   287  			return nil, trace.BadParameter("expected types.KubeServer, got: %T", resource)
   288  		}
   289  		servers = append(servers, server)
   290  	}
   291  	return servers, nil
   292  }
   293  
   294  // AsUserGroups converts each resource into type UserGroup.
   295  func (r ResourcesWithLabels) AsUserGroups() ([]UserGroup, error) {
   296  	userGroups := make([]UserGroup, 0, len(r))
   297  	for _, resource := range r {
   298  		userGroup, ok := resource.(UserGroup)
   299  		if !ok {
   300  			return nil, trace.BadParameter("expected types.UserGroup, got: %T", resource)
   301  		}
   302  		userGroups = append(userGroups, userGroup)
   303  	}
   304  	return userGroups, nil
   305  }
   306  
   307  // GetVersion returns resource version
   308  func (h *ResourceHeader) GetVersion() string {
   309  	return h.Version
   310  }
   311  
   312  // GetResourceID returns resource ID
   313  // Deprecated: Use GetRevision instead.
   314  func (h *ResourceHeader) GetResourceID() int64 {
   315  	return h.Metadata.ID
   316  }
   317  
   318  // SetResourceID sets resource ID
   319  // Deprecated: Use SetRevision instead.
   320  func (h *ResourceHeader) SetResourceID(id int64) {
   321  	h.Metadata.ID = id
   322  }
   323  
   324  // GetRevision returns the revision
   325  func (h *ResourceHeader) GetRevision() string {
   326  	return h.Metadata.GetRevision()
   327  }
   328  
   329  // SetRevision sets the revision
   330  func (h *ResourceHeader) SetRevision(rev string) {
   331  	h.Metadata.SetRevision(rev)
   332  }
   333  
   334  // GetName returns the name of the resource
   335  func (h *ResourceHeader) GetName() string {
   336  	return h.Metadata.Name
   337  }
   338  
   339  // SetName sets the name of the resource
   340  func (h *ResourceHeader) SetName(v string) {
   341  	h.Metadata.SetName(v)
   342  }
   343  
   344  // Expiry returns object expiry setting
   345  func (h *ResourceHeader) Expiry() time.Time {
   346  	return h.Metadata.Expiry()
   347  }
   348  
   349  // SetExpiry sets object expiry
   350  func (h *ResourceHeader) SetExpiry(t time.Time) {
   351  	h.Metadata.SetExpiry(t)
   352  }
   353  
   354  // GetMetadata returns object metadata
   355  func (h *ResourceHeader) GetMetadata() Metadata {
   356  	return h.Metadata
   357  }
   358  
   359  // GetKind returns resource kind
   360  func (h *ResourceHeader) GetKind() string {
   361  	return h.Kind
   362  }
   363  
   364  // GetSubKind returns resource subkind
   365  func (h *ResourceHeader) GetSubKind() string {
   366  	return h.SubKind
   367  }
   368  
   369  // SetSubKind sets resource subkind
   370  func (h *ResourceHeader) SetSubKind(s string) {
   371  	h.SubKind = s
   372  }
   373  
   374  // Origin returns the origin value of the resource.
   375  func (h *ResourceHeader) Origin() string {
   376  	return h.Metadata.Origin()
   377  }
   378  
   379  // SetOrigin sets the origin value of the resource.
   380  func (h *ResourceHeader) SetOrigin(origin string) {
   381  	h.Metadata.SetOrigin(origin)
   382  }
   383  
   384  // GetStaticLabels returns the static labels for the resource.
   385  func (h *ResourceHeader) GetStaticLabels() map[string]string {
   386  	return h.Metadata.Labels
   387  }
   388  
   389  // SetStaticLabels sets the static labels for the resource.
   390  func (h *ResourceHeader) SetStaticLabels(sl map[string]string) {
   391  	h.Metadata.Labels = sl
   392  }
   393  
   394  // GetLabel retrieves the label with the provided key. If not found
   395  // value will be empty and ok will be false.
   396  func (h *ResourceHeader) GetLabel(key string) (value string, ok bool) {
   397  	v, ok := h.Metadata.Labels[key]
   398  	return v, ok
   399  }
   400  
   401  // GetAllLabels returns all labels from the resource..
   402  func (h *ResourceHeader) GetAllLabels() map[string]string {
   403  	return h.Metadata.Labels
   404  }
   405  
   406  // IsEqual determines if two resource header resources are equivalent to one another.
   407  func (h *ResourceHeader) IsEqual(other *ResourceHeader) bool {
   408  	return deriveTeleportEqualResourceHeader(h, other)
   409  }
   410  
   411  func (h *ResourceHeader) CheckAndSetDefaults() error {
   412  	if h.Kind == "" {
   413  		return trace.BadParameter("resource has an empty Kind field")
   414  	}
   415  	if h.Version == "" {
   416  		return trace.BadParameter("resource has an empty Version field")
   417  	}
   418  	return trace.Wrap(h.Metadata.CheckAndSetDefaults())
   419  }
   420  
   421  // GetID returns resource ID
   422  func (m *Metadata) GetID() int64 {
   423  	return m.ID
   424  }
   425  
   426  // SetID sets resource ID
   427  func (m *Metadata) SetID(id int64) {
   428  	m.ID = id
   429  }
   430  
   431  // GetRevision returns the revision
   432  func (m *Metadata) GetRevision() string {
   433  	return m.Revision
   434  }
   435  
   436  // SetRevision sets the revision
   437  func (m *Metadata) SetRevision(rev string) {
   438  	m.Revision = rev
   439  }
   440  
   441  // GetMetadata returns object metadata
   442  func (m *Metadata) GetMetadata() Metadata {
   443  	return *m
   444  }
   445  
   446  // GetName returns the name of the resource
   447  func (m *Metadata) GetName() string {
   448  	return m.Name
   449  }
   450  
   451  // SetName sets the name of the resource
   452  func (m *Metadata) SetName(name string) {
   453  	m.Name = name
   454  }
   455  
   456  // SetExpiry sets expiry time for the object
   457  func (m *Metadata) SetExpiry(expires time.Time) {
   458  	m.Expires = &expires
   459  }
   460  
   461  // Expiry returns object expiry setting.
   462  func (m *Metadata) Expiry() time.Time {
   463  	if m.Expires == nil {
   464  		return time.Time{}
   465  	}
   466  	return *m.Expires
   467  }
   468  
   469  // Origin returns the origin value of the resource.
   470  func (m *Metadata) Origin() string {
   471  	if m.Labels == nil {
   472  		return ""
   473  	}
   474  	return m.Labels[OriginLabel]
   475  }
   476  
   477  // SetOrigin sets the origin value of the resource.
   478  func (m *Metadata) SetOrigin(origin string) {
   479  	if m.Labels == nil {
   480  		m.Labels = map[string]string{}
   481  	}
   482  	m.Labels[OriginLabel] = origin
   483  }
   484  
   485  // IsEqual determines if two metadata resources are equivalent to one another.
   486  func (m *Metadata) IsEqual(other *Metadata) bool {
   487  	return deriveTeleportEqualMetadata(m, other)
   488  }
   489  
   490  // CheckAndSetDefaults checks validity of all parameters and sets defaults
   491  func (m *Metadata) CheckAndSetDefaults() error {
   492  	if m.Name == "" {
   493  		return trace.BadParameter("missing parameter Name")
   494  	}
   495  	if m.Namespace == "" {
   496  		m.Namespace = defaults.Namespace
   497  	}
   498  
   499  	// adjust expires time to UTC if it's set
   500  	if m.Expires != nil {
   501  		utils.UTC(m.Expires)
   502  	}
   503  
   504  	for key := range m.Labels {
   505  		if !IsValidLabelKey(key) {
   506  			return trace.BadParameter("invalid label key: %q", key)
   507  		}
   508  	}
   509  
   510  	// Check the origin value.
   511  	if m.Origin() != "" {
   512  		if !slices.Contains(OriginValues, m.Origin()) {
   513  			return trace.BadParameter("invalid origin value %q, must be one of %v", m.Origin(), OriginValues)
   514  		}
   515  	}
   516  
   517  	return nil
   518  }
   519  
   520  // MatchLabels takes a map of labels and returns `true` if the resource has ALL
   521  // of them.
   522  func MatchLabels(resource ResourceWithLabels, labels map[string]string) bool {
   523  	for key, value := range labels {
   524  		if v, ok := resource.GetLabel(key); !ok || v != value {
   525  			return false
   526  		}
   527  	}
   528  
   529  	return true
   530  }
   531  
   532  // MatchKinds takes an array of strings that represent a Kind and
   533  // returns true if the resource's kind matches any item in the given array.
   534  func MatchKinds(resource ResourceWithLabels, kinds []string) bool {
   535  	if len(kinds) == 0 {
   536  		return true
   537  	}
   538  	resourceKind := resource.GetKind()
   539  	switch resourceKind {
   540  	case KindApp, KindSAMLIdPServiceProvider:
   541  		return slices.Contains(kinds, KindApp)
   542  	default:
   543  		return slices.Contains(kinds, resourceKind)
   544  	}
   545  }
   546  
   547  // IsValidLabelKey checks if the supplied string matches the
   548  // label key regexp.
   549  func IsValidLabelKey(s string) bool {
   550  	return common.IsValidLabelKey(s)
   551  }
   552  
   553  // MatchSearch goes through select field values from a resource
   554  // and tries to match against the list of search values, ignoring case and order.
   555  // Returns true if all search vals were matched (or if nil search vals).
   556  // Returns false if no or partial match (or nil field values).
   557  func MatchSearch(fieldVals []string, searchVals []string, customMatch func(val string) bool) bool {
   558  Outer:
   559  	for _, searchV := range searchVals {
   560  		// Iterate through field values to look for a match.
   561  		for _, fieldV := range fieldVals {
   562  			if containsFold(fieldV, searchV) {
   563  				continue Outer
   564  			}
   565  		}
   566  
   567  		if customMatch != nil && customMatch(searchV) {
   568  			continue
   569  		}
   570  
   571  		// When no fields matched a value, prematurely end if we can.
   572  		return false
   573  	}
   574  
   575  	return true
   576  }
   577  
   578  // containsFold is a case-insensitive alternative to strings.Contains, used to help avoid excess allocations during searches.
   579  func containsFold(s, substr string) bool {
   580  	if len(s) < len(substr) {
   581  		return false
   582  	}
   583  
   584  	n := len(s) - len(substr)
   585  
   586  	for i := 0; i <= n; i++ {
   587  		if strings.EqualFold(s[i:i+len(substr)], substr) {
   588  			return true
   589  		}
   590  	}
   591  
   592  	return false
   593  }
   594  
   595  func stringCompare(a string, b string, isDesc bool) bool {
   596  	if isDesc {
   597  		return a > b
   598  	}
   599  	return a < b
   600  }
   601  
   602  var kindsOrder = []string{
   603  	"app", "db", "windows_desktop", "kube_cluster", "node",
   604  }
   605  
   606  // unifiedKindCompare compares two resource kinds and returns true if a is less than b.
   607  // Note that it's not just a simple string comparison, since the UI names these
   608  // kinds slightly differently, and hence uses a different alphabetical order for
   609  // them.
   610  //
   611  // If resources are of the same kind, this function falls back to comparing
   612  // their unified names.
   613  func unifiedKindCompare(a, b ResourceWithLabels, isDesc bool) bool {
   614  	ak := a.GetKind()
   615  	bk := b.GetKind()
   616  
   617  	if ak == bk {
   618  		return unifiedNameCompare(a, b, isDesc)
   619  	}
   620  
   621  	ia := slices.Index(kindsOrder, ak)
   622  	ib := slices.Index(kindsOrder, bk)
   623  	if ia < 0 && ib < 0 {
   624  		// Fallback for a case of two unknown resources.
   625  		return stringCompare(ak, bk, isDesc)
   626  	}
   627  	if isDesc {
   628  		return ia > ib
   629  	}
   630  	return ia < ib
   631  }
   632  
   633  func unifiedNameCompare(a ResourceWithLabels, b ResourceWithLabels, isDesc bool) bool {
   634  	var nameA, nameB string
   635  	switch r := a.(type) {
   636  	case AppServer:
   637  		nameA = r.GetApp().GetName()
   638  	case DatabaseServer:
   639  		nameA = r.GetDatabase().GetName()
   640  	case KubeServer:
   641  		nameA = r.GetCluster().GetName()
   642  	case Server:
   643  		nameA = r.GetHostname()
   644  	default:
   645  		nameA = a.GetName()
   646  	}
   647  
   648  	switch r := b.(type) {
   649  	case AppServer:
   650  		nameB = r.GetApp().GetName()
   651  	case DatabaseServer:
   652  		nameB = r.GetDatabase().GetName()
   653  	case KubeServer:
   654  		nameB = r.GetCluster().GetName()
   655  	case Server:
   656  		nameB = r.GetHostname()
   657  	default:
   658  		nameB = a.GetName()
   659  	}
   660  
   661  	return stringCompare(strings.ToLower(nameA), strings.ToLower(nameB), isDesc)
   662  }
   663  
   664  func (r ResourcesWithLabels) SortByCustom(by SortBy) error {
   665  	isDesc := by.IsDesc
   666  	switch by.Field {
   667  	case ResourceMetadataName:
   668  		sort.SliceStable(r, func(i, j int) bool {
   669  			return unifiedNameCompare(r[i], r[j], isDesc)
   670  		})
   671  	case ResourceKind:
   672  		sort.SliceStable(r, func(i, j int) bool {
   673  			return unifiedKindCompare(r[i], r[j], isDesc)
   674  		})
   675  	default:
   676  		return trace.NotImplemented("sorting by field %q for unified resource %q is not supported", by.Field, KindUnifiedResource)
   677  	}
   678  	return nil
   679  }
   680  
   681  // ListResourcesResponse describes a non proto response to ListResources.
   682  type ListResourcesResponse struct {
   683  	// Resources is a list of resource.
   684  	Resources []ResourceWithLabels
   685  	// NextKey is the next key to use as a starting point.
   686  	NextKey string
   687  	// TotalCount is the total number of resources available as a whole.
   688  	TotalCount int
   689  }
   690  
   691  // ValidateResourceName validates a resource name using a given regexp.
   692  func ValidateResourceName(validationRegex *regexp.Regexp, name string) error {
   693  	if validationRegex.MatchString(name) {
   694  		return nil
   695  	}
   696  	return trace.BadParameter(
   697  		"%q does not match regex used for validation %q",
   698  		name, validationRegex.String(),
   699  	)
   700  }
   701  
   702  // FriendlyName will return the friendly name for a resource if it has one. Otherwise, it
   703  // will return an empty string.
   704  func FriendlyName(resource ResourceWithLabels) string {
   705  	// Right now, only resources sourced from Okta and nodes have friendly names.
   706  	if resource.Origin() == OriginOkta {
   707  		if appName, ok := resource.GetLabel(OktaAppNameLabel); ok {
   708  			return appName
   709  		} else if groupName, ok := resource.GetLabel(OktaGroupNameLabel); ok {
   710  			return groupName
   711  		} else if roleName, ok := resource.GetLabel(OktaRoleNameLabel); ok {
   712  			return roleName
   713  		}
   714  		return resource.GetMetadata().Description
   715  	}
   716  
   717  	if hn, ok := resource.(interface{ GetHostname() string }); ok {
   718  		return hn.GetHostname()
   719  	}
   720  
   721  	return ""
   722  }
   723  
   724  // GetOrigin returns the value set for the [OriginLabel].
   725  // If the label is missing, an empty string is returned.
   726  //
   727  // Works for both [ResourceWithOrigin] and [ResourceMetadata] instances.
   728  func GetOrigin(v any) (string, error) {
   729  	switch r := v.(type) {
   730  	case ResourceWithOrigin:
   731  		return r.Origin(), nil
   732  	case ResourceMetadata:
   733  		meta := r.GetMetadata()
   734  		if meta.Labels == nil {
   735  			return "", nil
   736  		}
   737  		return meta.Labels[OriginLabel], nil
   738  	}
   739  	return "", trace.BadParameter("unable to determine origin from resource of type %T", v)
   740  }
   741  
   742  // GetKind returns the kind, if one can be obtained, otherwise
   743  // an empty string is returned.
   744  //
   745  // Works for both [Resource] and [ResourceMetadata] instances.
   746  func GetKind(v any) (string, error) {
   747  	type kinder interface {
   748  		GetKind() string
   749  	}
   750  	if k, ok := v.(kinder); ok {
   751  		return k.GetKind(), nil
   752  	}
   753  	return "", trace.BadParameter("unable to determine kind from resource of type %T", v)
   754  }
   755  
   756  // GetRevision returns the revision, if one can be obtained, otherwise
   757  // an empty string is returned.
   758  //
   759  // Works for both [Resource] and [ResourceMetadata] instances.
   760  func GetRevision(v any) (string, error) {
   761  	switch r := v.(type) {
   762  	case Resource:
   763  		return r.GetRevision(), nil
   764  	case ResourceMetadata:
   765  		return r.GetMetadata().Revision, nil
   766  	}
   767  	return "", trace.BadParameter("unable to determine revision from resource of type %T", v)
   768  }
   769  
   770  // SetRevision updates the revision if v supports the concept of revisions.
   771  //
   772  // Works for both [Resource] and [ResourceMetadata] instances.
   773  func SetRevision(v any, revision string) error {
   774  	switch r := v.(type) {
   775  	case Resource:
   776  		r.SetRevision(revision)
   777  		return nil
   778  	case ResourceMetadata:
   779  		r.GetMetadata().Revision = revision
   780  		return nil
   781  	}
   782  	return trace.BadParameter("unable to set revision on resource of type %T", v)
   783  }
   784  
   785  // GetExpiry returns the expiration, if one can be obtained, otherwise returns
   786  // an empty time `time.Time{}`, which is equivalent to no expiry.
   787  //
   788  // Works for both [Resource] and [ResourceMetadata] instances.
   789  func GetExpiry(v any) (time.Time, error) {
   790  	switch r := v.(type) {
   791  	case Resource:
   792  		return r.Expiry(), nil
   793  	case ResourceMetadata:
   794  		// ResourceMetadata uses *timestamppb.Timestamp instead of time.Time. The zero value for this type is 01/01/1970.
   795  		// This is a problem for resources without explicit expiry set: they'd become obsolete on creation.
   796  		// For this reason, we check for nil expiry explicitly, and default it to time.Time{}.
   797  		exp := r.GetMetadata().GetExpires()
   798  		if exp == nil {
   799  			return time.Time{}, nil
   800  		}
   801  		return exp.AsTime(), nil
   802  	}
   803  	return time.Time{}, trace.BadParameter("unable to determine expiry from resource of type %T", v)
   804  }
   805  
   806  // GetResourceID returns the id, if one can be obtained, otherwise returns
   807  // zero.
   808  //
   809  // Works for both [Resource] and [ResourceMetadata] instances.
   810  //
   811  // Deprecated: GetRevision should be used instead.
   812  func GetResourceID(v any) (int64, error) {
   813  	switch r := v.(type) {
   814  	case Resource:
   815  		//nolint:staticcheck // SA1019. Added for backward compatibility.
   816  		return r.GetResourceID(), nil
   817  	case ResourceMetadata:
   818  		//nolint:staticcheck // SA1019. Added for backward compatibility.
   819  		return r.GetMetadata().Id, nil
   820  	}
   821  	return 0, trace.BadParameter("unable to determine resource ID from resource of type %T", v)
   822  }