diff --git a/api/go.mod b/api/go.mod index d6fcb567171b8..34fe281966860 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,6 +3,7 @@ module github.com/gravitational/teleport/api go 1.23.0 require ( + github.com/charlievieth/strcase v0.0.5 github.com/coreos/go-semver v0.3.1 github.com/go-piv/piv-go v1.11.0 github.com/gobwas/ws v1.4.0 diff --git a/api/go.sum b/api/go.sum index 2c4a01fec86f2..1db0f917732ec 100644 --- a/api/go.sum +++ b/api/go.sum @@ -3,6 +3,8 @@ github.com/beevik/etree v1.3.0 h1:hQTc+pylzIKDb23yYprodCWWTt+ojFfUZyzU09a/hmU= github.com/beevik/etree v1.3.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/charlievieth/strcase v0.0.5 h1:gV4iXVyD6eI5KdfOV+/vIVCKXZwtCWOmDMcu7Uy00Rs= +github.com/charlievieth/strcase v0.0.5/go.mod h1:FIOYY1aDBMSIOFqmVomHBpoK+bteGlESRsgsdWjrhx8= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/api/types/lock.go b/api/types/lock.go index 5a35c2a167860..4aaeb74aa8cc3 100644 --- a/api/types/lock.go +++ b/api/types/lock.go @@ -298,5 +298,13 @@ func (t LockTarget) String() string { // Equals returns true when the two lock targets are equal. func (t LockTarget) Equals(t2 LockTarget) bool { - return proto.Equal(&t, &t2) + return t.User == t2.User && + t.Role == t2.Role && + t.Login == t2.Login && + t.MFADevice == t2.MFADevice && + t.WindowsDesktop == t2.WindowsDesktop && + t.AccessRequest == t2.AccessRequest && + t.Device == t2.Device && + t.ServerID == t2.ServerID && + t.Node == t2.Node } diff --git a/api/types/lock_test.go b/api/types/lock_test.go index 020fa4d56513f..2e76c6225e19c 100644 --- a/api/types/lock_test.go +++ b/api/types/lock_test.go @@ -133,3 +133,39 @@ func TestLockTargetIsEmpty(t *testing.T) { require.False(t, lt.IsEmpty(), "field name: %v", field.Name) } } + +// TestLockTargetEquals checks that the implementation of [LockTarget.Equals] +// is correct by filling one field at a time in for two LockTargets and expecting +// Equals to return the appropriate value. Only the public fields that don't start with +// `XXX_` are checked (as those are gogoproto-internal fields). +func TestLockTargetEquals(t *testing.T) { + t.Run("equal", func(t *testing.T) { + require.True(t, (LockTarget{}).Equals(LockTarget{}), "empty targets equal") + + for i, field := range reflect.VisibleFields(reflect.TypeOf(LockTarget{})) { + if strings.HasPrefix(field.Name, "XXX_") { + continue + } + + var a, b LockTarget + // if we add non-string fields to LockTarget we need a type switch here + reflect.ValueOf(&a).Elem().Field(i).SetString("nonempty") + reflect.ValueOf(&b).Elem().Field(i).SetString("nonempty") + require.True(t, a.Equals(b), "field name: %v", field.Name) + } + }) + + t.Run("not equal", func(t *testing.T) { + for i, field := range reflect.VisibleFields(reflect.TypeOf(LockTarget{})) { + if strings.HasPrefix(field.Name, "XXX_") { + continue + } + + var a, b LockTarget + // if we add non-string fields to LockTarget we need a type switch here + reflect.ValueOf(&a).Elem().Field(i).SetString("nonempty") + reflect.ValueOf(&b).Elem().Field(i).SetString("other") + require.False(t, a.Equals(b), "field name: %v", field.Name) + } + }) +} diff --git a/api/types/resource.go b/api/types/resource.go index e5d68768a7c5a..97c808247fd4a 100644 --- a/api/types/resource.go +++ b/api/types/resource.go @@ -23,6 +23,7 @@ import ( "strings" "time" + "github.com/charlievieth/strcase" "github.com/gravitational/trace" "github.com/gravitational/teleport/api/defaults" @@ -544,7 +545,7 @@ Outer: for _, searchV := range searchVals { // Iterate through field values to look for a match. for _, fieldV := range fieldVals { - if containsFold(fieldV, searchV) { + if strcase.Contains(fieldV, searchV) { continue Outer } } @@ -560,23 +561,6 @@ Outer: return true } -// containsFold is a case-insensitive alternative to strings.Contains, used to help avoid excess allocations during searches. -func containsFold(s, substr string) bool { - if len(s) < len(substr) { - return false - } - - n := len(s) - len(substr) - - for i := 0; i <= n; i++ { - if strings.EqualFold(s[i:i+len(substr)], substr) { - return true - } - } - - return false -} - func stringCompare(a string, b string, isDesc bool) bool { if isDesc { return a > b diff --git a/api/types/server.go b/api/types/server.go index 5d3af19c33c24..d7696a1d7c508 100644 --- a/api/types/server.go +++ b/api/types/server.go @@ -23,6 +23,7 @@ import ( "strings" "time" + "github.com/charlievieth/strcase" "github.com/google/uuid" "github.com/gravitational/trace" @@ -652,25 +653,46 @@ func (s *ServerV2) MatchSearch(values []string) bool { if s.GetKind() != KindNode { return false } +Outer: + for _, searchV := range values { + for key, value := range s.Metadata.Labels { + if strcase.Contains(key, searchV) || strcase.Contains(value, searchV) { + continue Outer + } + } + for key, cmd := range s.Spec.CmdLabels { + if strcase.Contains(key, searchV) || strcase.Contains(cmd.Result, searchV) { + continue Outer + } + } - var custom func(val string) bool - if s.GetUseTunnel() { - custom = func(val string) bool { - return strings.EqualFold(val, "tunnel") + if strcase.Contains(s.Metadata.Name, searchV) { + continue } - } - fieldVals := make([]string, 0, (len(s.Metadata.Labels)*2)+(len(s.Spec.CmdLabels)*2)+len(s.Spec.PublicAddrs)+3) + if strcase.Contains(s.Spec.Hostname, searchV) { + continue + } - labels := CombineLabels(s.Metadata.Labels, s.Spec.CmdLabels) - for key, value := range labels { - fieldVals = append(fieldVals, key, value) - } + if strcase.Contains(s.Spec.Addr, searchV) { + continue + } + + for _, addr := range s.Spec.PublicAddrs { + if strcase.Contains(addr, searchV) { + continue Outer + } + } - fieldVals = append(fieldVals, s.Metadata.Name, s.Spec.Hostname, s.Spec.Addr) - fieldVals = append(fieldVals, s.Spec.PublicAddrs...) + if s.GetUseTunnel() && strings.EqualFold(searchV, "tunnel") { + continue + } + + // When no fields matched a value, prematurely end if we can. + return false + } - return MatchSearch(fieldVals, values, custom) + return true } // DeepCopy creates a clone of this server value diff --git a/go.mod b/go.mod index 8168aaed40da1..1d365e9305b37 100644 --- a/go.mod +++ b/go.mod @@ -298,6 +298,7 @@ require ( github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/charlievieth/strcase v0.0.5 // indirect github.com/charmbracelet/x/ansi v0.2.3 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.0 // indirect diff --git a/go.sum b/go.sum index 0ced1f3905cd3..e91696948e0cc 100644 --- a/go.sum +++ b/go.sum @@ -1005,6 +1005,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/charlievieth/strcase v0.0.5 h1:gV4iXVyD6eI5KdfOV+/vIVCKXZwtCWOmDMcu7Uy00Rs= +github.com/charlievieth/strcase v0.0.5/go.mod h1:FIOYY1aDBMSIOFqmVomHBpoK+bteGlESRsgsdWjrhx8= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= diff --git a/integrations/event-handler/go.mod b/integrations/event-handler/go.mod index b84fd87ae0ab7..d5a9febcbd576 100644 --- a/integrations/event-handler/go.mod +++ b/integrations/event-handler/go.mod @@ -102,6 +102,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/charlievieth/strcase v0.0.5 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/cfssl v1.6.4 // indirect github.com/containerd/containerd v1.7.27 // indirect diff --git a/integrations/event-handler/go.sum b/integrations/event-handler/go.sum index e7443d12382e5..250f6e195c735 100644 --- a/integrations/event-handler/go.sum +++ b/integrations/event-handler/go.sum @@ -212,6 +212,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/charlievieth/strcase v0.0.5 h1:gV4iXVyD6eI5KdfOV+/vIVCKXZwtCWOmDMcu7Uy00Rs= +github.com/charlievieth/strcase v0.0.5/go.mod h1:FIOYY1aDBMSIOFqmVomHBpoK+bteGlESRsgsdWjrhx8= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= diff --git a/integrations/terraform/go.mod b/integrations/terraform/go.mod index b5700ed88f8a3..c52cabfbfe001 100644 --- a/integrations/terraform/go.mod +++ b/integrations/terraform/go.mod @@ -117,6 +117,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/charlievieth/strcase v0.0.5 // indirect github.com/cloudflare/cfssl v1.6.4 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59 // indirect diff --git a/integrations/terraform/go.sum b/integrations/terraform/go.sum index 1db21485994e1..dffa70fd7fd54 100644 --- a/integrations/terraform/go.sum +++ b/integrations/terraform/go.sum @@ -323,6 +323,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/charlievieth/strcase v0.0.5 h1:gV4iXVyD6eI5KdfOV+/vIVCKXZwtCWOmDMcu7Uy00Rs= +github.com/charlievieth/strcase v0.0.5/go.mod h1:FIOYY1aDBMSIOFqmVomHBpoK+bteGlESRsgsdWjrhx8= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= diff --git a/lib/auth/auth_test.go b/lib/auth/auth_test.go index 51474a255888d..319fc27204658 100644 --- a/lib/auth/auth_test.go +++ b/lib/auth/auth_test.go @@ -3049,26 +3049,16 @@ func TestGenerateKubernetesUserCert(t *testing.T) { // Wait for cache propagation of the kubernetes resources before proceeding with the tests. require.EventuallyWithT(t, func(t *assert.CollectT) { - found, _, err := p.a.UnifiedResourceCache.IterateUnifiedResources(ctx, func(rwl types.ResourceWithLabels) (bool, error) { - if rwl.GetKind() != types.KindKubeServer { - return false, nil - } - - ks, ok := rwl.(types.KubeServer) - if !ok { - return false, nil + gotNames := map[string]struct{}{} + for ks, err := range p.a.UnifiedResourceCache.KubernetesServers(ctx, services.UnifiedResourcesIterateParams{}) { + if !assert.NoError(t, err) { + return } - return ks.GetCluster().GetName() == kubeCluster.GetName(), nil - }, &proto.ListUnifiedResourcesRequest{ - Kinds: []string{types.KindKubeServer}, - SortBy: types.SortBy{Field: services.SortByName}, - Limit: 1, - }) - - assert.NoError(t, err) - assert.Len(t, found, 1) - }, 10*time.Second, 100*time.Millisecond) + gotNames[ks.GetCluster().GetName()] = struct{}{} + } + assert.Contains(t, gotNames, kubeCluster.GetName(), "missing kube cluster") + }, 15*time.Second, 100*time.Millisecond) accessInfo := services.AccessInfoFromUserState(user) accessChecker, err := services.NewAccessChecker(accessInfo, p.clusterName.GetClusterName(), p.a) diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 4f58bdc34c13b..b1573425c0629 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -22,6 +22,7 @@ import ( "cmp" "context" "fmt" + "maps" "net/url" "os" "slices" @@ -1334,6 +1335,22 @@ func (a *ServerWithRoles) selectActionChecker(resourceKind string) actionChecker return a.action } +var ( + // supportedUnifiedResourceKinds is the set of kinds that + // may be requested via ListUnifiedResources. + supportedUnifiedResourceKinds = map[string]struct{}{ + types.KindApp: {}, + types.KindDatabase: {}, + types.KindGitServer: {}, + types.KindKubernetesCluster: {}, + types.KindNode: {}, + types.KindSAMLIdPServiceProvider: {}, + types.KindWindowsDesktop: {}, + } + + defaultUnifiedResourceKinds = slices.Collect(maps.Keys(supportedUnifiedResourceKinds)) +) + // ListUnifiedResources returns a paginated list of unified resources filtered by user access. func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.ListUnifiedResourcesRequest) (*proto.ListUnifiedResourcesResponse, error) { filter := services.MatchResourceFilter{ @@ -1350,17 +1367,29 @@ func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.L filter.PredicateExpression = expression } + // Validate the requested kinds and precheck that the user has read/list + // permissions for all requested resources before doing any listing of + // resources to conserve resources. + requested := req.Kinds + if len(req.Kinds) == 0 { + requested = defaultUnifiedResourceKinds + } + resourceAccess := &resourceAccess{ // Populate kindAccessMap with any access errors the user has for each possible // resource kind. This allows the access check to occur a single time per resource // kind instead of once per matching resource. - kindAccessMap: make(map[string]error, len(services.UnifiedResourceKinds)), + kindAccessMap: make(map[string]error, len(requested)), // requestableMap is populated with resources that are being returned but can only // be accessed to the user via an access request requestableMap: make(map[string]struct{}), } - for _, kind := range services.UnifiedResourceKinds { + for _, kind := range requested { + if _, ok := supportedUnifiedResourceKinds[kind]; !ok { + return nil, trace.BadParameter("Unsupported kind %q requested", kind) + } + actionVerbs := []string{types.VerbList, types.VerbRead} if kind == types.KindNode { // We are checking list only for Nodes to keep backwards compatibility. @@ -1378,10 +1407,6 @@ func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.L // Before doing any listing, verify that the user is allowed to list // at least one of the requested kinds. If no access is permitted, then // return an access denied error. - requested := req.Kinds - if len(req.Kinds) == 0 { - requested = services.UnifiedResourceKinds - } var rbacErrors int for _, kind := range requested { if err, ok := resourceAccess.kindAccessMap[kind]; ok && err != nil { diff --git a/lib/auth/auth_with_roles_test.go b/lib/auth/auth_with_roles_test.go index a21d5ecefb3c5..cbfc902ad7f50 100644 --- a/lib/auth/auth_with_roles_test.go +++ b/lib/auth/auth_with_roles_test.go @@ -5823,7 +5823,7 @@ func TestListUnifiedResources_MixedAccess(t *testing.T) { } // Update the roles to prevent access to any resource kinds. - role.SetRules(types.Deny, []types.Rule{{Resources: services.UnifiedResourceKinds, Verbs: []string{types.VerbList, types.VerbRead}}}) + role.SetRules(types.Deny, []types.Rule{{Resources: []string{types.Wildcard}, Verbs: []string{types.VerbList, types.VerbRead}}}) _, err = srv.Auth().UpsertRole(ctx, role) require.NoError(t, err) @@ -5847,7 +5847,7 @@ func TestListUnifiedResources_MixedAccess(t *testing.T) { resp, err = clt.ListUnifiedResources(ctx, &proto.ListUnifiedResourcesRequest{ Limit: 20, SortBy: types.SortBy{IsDesc: true, Field: types.ResourceMetadataName}, - Kinds: []string{types.KindNode, types.KindDatabaseServer}, + Kinds: []string{types.KindNode, types.KindDatabase}, }) require.True(t, trace.IsAccessDenied(err)) require.Nil(t, resp) @@ -6271,7 +6271,9 @@ func BenchmarkListUnifiedResourcesFilter(b *testing.B) { node, err := types.NewServerWithLabels( name, types.KindNode, - types.ServerSpecV2{}, + types.ServerSpecV2{ + Hostname: "node." + strconv.Itoa(i), + }, labels, ) require.NoError(b, err) @@ -6392,7 +6394,9 @@ func BenchmarkListUnifiedResources(b *testing.B) { node, err := types.NewServerWithLabels( name, types.KindNode, - types.ServerSpecV2{}, + types.ServerSpecV2{ + Hostname: "node." + strconv.Itoa(i), + }, map[string]string{ "key": id, "group": "users", @@ -6469,6 +6473,7 @@ func benchmarkListUnifiedResources( for n := 0; n < b.N; n++ { var resources []*proto.PaginatedResource req := &proto.ListUnifiedResourcesRequest{ + Kinds: []string{types.KindNode}, SortBy: types.SortBy{IsDesc: false, Field: types.ResourceMetadataName}, Limit: 1_000, } diff --git a/lib/services/unified_resource.go b/lib/services/unified_resource.go index 23326b6fd6554..5ff2753b5aeec 100644 --- a/lib/services/unified_resource.go +++ b/lib/services/unified_resource.go @@ -43,19 +43,6 @@ import ( "github.com/gravitational/teleport/lib/utils/pagination" ) -// UnifiedResourceKinds is a list of all kinds that are stored in the unified resource cache. -var UnifiedResourceKinds = []string{ - types.KindNode, - types.KindKubeServer, - types.KindDatabaseServer, - types.KindAppServer, - types.KindWindowsDesktop, - types.KindSAMLIdPServiceProvider, - types.KindIdentityCenterAccount, - types.KindIdentityCenterAccountAssignment, - types.KindGitServer, -} - // UnifiedResourceCacheConfig is used to configure a UnifiedResourceCache type UnifiedResourceCacheConfig struct { // BTreeDegree is a degree of B-Tree, 2 for example, will create a @@ -994,12 +981,17 @@ func (c *UnifiedResourceCache) processEventsAndUpdateCurrent(ctx context.Context // resourceKinds returns a list of resources to be watched. func (c *UnifiedResourceCache) resourceKinds() []types.WatchKind { - watchKinds := make([]types.WatchKind, 0, len(UnifiedResourceKinds)) - for _, kind := range UnifiedResourceKinds { - watchKinds = append(watchKinds, types.WatchKind{Kind: kind}) + return []types.WatchKind{ + {Kind: types.KindNode}, + {Kind: types.KindKubeServer}, + {Kind: types.KindDatabaseServer}, + {Kind: types.KindAppServer}, + {Kind: types.KindWindowsDesktop}, + {Kind: types.KindSAMLIdPServiceProvider}, + {Kind: types.KindIdentityCenterAccount}, + {Kind: types.KindIdentityCenterAccountAssignment}, + {Kind: types.KindGitServer}, } - - return watchKinds } func (c *UnifiedResourceCache) defineCollectorAsInitialized() { diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 987fdc12ab2ba..728c831d09e28 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -2628,27 +2628,18 @@ func TestKubeCredentialsLock(t *testing.T) { require.NoError(t, err) require.EventuallyWithT(t, func(t *assert.CollectT) { - found, _, err := authServer.UnifiedResourceCache.IterateUnifiedResources(ctx, func(rwl types.ResourceWithLabels) (bool, error) { - if rwl.GetKind() != types.KindKubeServer { - return false, nil + gotNames := map[string]struct{}{} + for ks, err := range authServer.UnifiedResourceCache.KubernetesServers(ctx, services.UnifiedResourcesIterateParams{}) { + if !assert.NoError(t, err) { + return } - ks, ok := rwl.(types.KubeServer) - if !ok { - return false, nil - } - - return ks.GetCluster().GetName() == kubeCluster.GetName(), nil - }, &proto.ListUnifiedResourcesRequest{ - Kinds: []string{types.KindKubeServer}, - SortBy: types.SortBy{Field: services.SortByName}, - Limit: 1, - }) + gotNames[ks.GetCluster().GetName()] = struct{}{} - assert.NoError(t, err) - assert.Len(t, found, 1) + } - }, 10*time.Second, 100*time.Millisecond) + assert.Contains(t, gotNames, kubeCluster.GetName(), "missing kube cluster") + }, 15*time.Second, 100*time.Millisecond) var ssoCalls atomic.Int32 mockSSOLogin := mockSSOLogin(authServer, alice)