Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 23 additions & 37 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3706,51 +3706,39 @@ type ResourcePage[T types.ResourceWithLabels] struct {
NextKey string
}

// getResourceFromProtoPage extracts the resource from the PaginatedResource returned
// from the rpc ListUnifiedResources
func getResourceFromProtoPage(resource *proto.PaginatedResource) (types.ResourceWithLabels, error) {
var out types.ResourceWithLabels
// convertEnrichedResource extracts the resource and any enriched information from the
// PaginatedResource returned from the rpc ListUnifiedResources.
func convertEnrichedResource(resource *proto.PaginatedResource) (*types.EnrichedResource, error) {
if r := resource.GetNode(); r != nil {
out = r
return out, nil
return &types.EnrichedResource{ResourceWithLabels: r, Logins: resource.Logins}, nil
} else if r := resource.GetDatabaseServer(); r != nil {
out = r
return out, nil
return &types.EnrichedResource{ResourceWithLabels: r}, nil
} else if r := resource.GetDatabaseService(); r != nil {
out = r
return out, nil
return &types.EnrichedResource{ResourceWithLabels: r}, nil
} else if r := resource.GetAppServerOrSAMLIdPServiceProvider(); r != nil { //nolint:staticcheck // SA1019. TODO(sshah) DELETE IN 17.0
out = r
return out, nil
return &types.EnrichedResource{ResourceWithLabels: r}, nil
} else if r := resource.GetWindowsDesktop(); r != nil {
out = r
return out, nil
return &types.EnrichedResource{ResourceWithLabels: r}, nil
} else if r := resource.GetWindowsDesktopService(); r != nil {
out = r
return out, nil
return &types.EnrichedResource{ResourceWithLabels: r}, nil
} else if r := resource.GetKubeCluster(); r != nil {
out = r
return out, nil
return &types.EnrichedResource{ResourceWithLabels: r}, nil
} else if r := resource.GetKubernetesServer(); r != nil {
out = r
return out, nil
return &types.EnrichedResource{ResourceWithLabels: r}, nil
} else if r := resource.GetUserGroup(); r != nil {
out = r
return out, nil
return &types.EnrichedResource{ResourceWithLabels: r}, nil
} else if r := resource.GetAppServer(); r != nil {
out = r
return out, nil
return &types.EnrichedResource{ResourceWithLabels: r}, nil
} else if r := resource.GetSAMLIdPServiceProvider(); r != nil {
out = r
return out, nil
return &types.EnrichedResource{ResourceWithLabels: r}, nil
} else {
return nil, trace.BadParameter("received unsupported resource %T", resource.Resource)
}
}

// ListUnifiedResourcePage is a helper for getting a single page of unified resources that match the provided request.
func ListUnifiedResourcePage(ctx context.Context, clt ListUnifiedResourcesClient, req *proto.ListUnifiedResourcesRequest) (ResourcePage[types.ResourceWithLabels], error) {
var out ResourcePage[types.ResourceWithLabels]
// GetUnifiedResourcePage is a helper for getting a single page of unified resources that match the provided request.
func GetUnifiedResourcePage(ctx context.Context, clt ListUnifiedResourcesClient, req *proto.ListUnifiedResourcesRequest) ([]*types.EnrichedResource, string, error) {
var out []*types.EnrichedResource

// Set the limit to the default size if one was not provided within
// an acceptable range.
Expand All @@ -3766,26 +3754,24 @@ func ListUnifiedResourcePage(ctx context.Context, clt ListUnifiedResourcesClient
req.Limit /= 2
// This is an extremely unlikely scenario, but better to cover it anyways.
if req.Limit == 0 {
return out, trace.Wrap(err, "resource is too large to retrieve")
return nil, "", trace.Wrap(err, "resource is too large to retrieve")
}

continue
}

return out, trace.Wrap(err)
return nil, "", trace.Wrap(err)
}

for _, respResource := range resp.Resources {
resource, err := getResourceFromProtoPage(respResource)
resource, err := convertEnrichedResource(respResource)
if err != nil {
return out, trace.Wrap(err)
return nil, "", trace.Wrap(err)
}
out.Resources = append(out.Resources, resource)
out = append(out, resource)
}

out.NextKey = resp.NextKey

return out, nil
return out, resp.NextKey, nil
}
}

Expand Down
8 changes: 4 additions & 4 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2414,13 +2414,13 @@ func (tc *TeleportClient) ListNodesWithFilters(ctx context.Context) ([]types.Ser

var servers []types.Server
for {
page, err := client.ListUnifiedResourcePage(ctx, clt.AuthClient, &req)
page, next, err := client.GetUnifiedResourcePage(ctx, clt.AuthClient, &req)
if err != nil {
return nil, trace.Wrap(err)
}

for _, r := range page.Resources {
srv, ok := r.(types.Server)
for _, r := range page {
srv, ok := r.ResourceWithLabels.(types.Server)
if !ok {
log.Warnf("expected types.Server but received unexpected type %T", r)
continue
Expand All @@ -2429,7 +2429,7 @@ func (tc *TeleportClient) ListNodesWithFilters(ctx context.Context) ([]types.Ser
servers = append(servers, srv)
}

req.StartKey = page.NextKey
req.StartKey = next
if req.StartKey == "" {
break
}
Expand Down
106 changes: 91 additions & 15 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import (
"github.com/gravitational/teleport/lib/secret"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/web/app"
websession "github.com/gravitational/teleport/lib/web/session"
Expand Down Expand Up @@ -2654,9 +2655,67 @@ func makeUnifiedResourceRequest(r *http.Request) (*proto.ListUnifiedResourcesReq
PredicateExpression: values.Get("query"),
SearchKeywords: client.ParseSearchKeywords(values.Get("search"), ' '),
UseSearchAsRoles: values.Get("searchAsRoles") == "yes",
IncludeLogins: true,
}, nil
}

type loginGetter interface {
GetAllowedLoginsForResource(resource services.AccessCheckable) ([]string, error)
}

// calculateSSHLogins returns the subset of the allowedLogins that exist in
// the principals of the identity. This is required because SSH authorization
// only allows using a login that exists in the certificates valid principals.
// When connecting to servers in a leaf cluster, the root certificate is used,
// so we need to ensure that we only present the allowed logins that would
// result in a successful connection, if any exists.
func calculateSSHLogins(identity *tlsca.Identity, loginGetter loginGetter, r types.ResourceWithLabels, allowedLogins []string) ([]string, error) {
// TODO(tross) DELETE IN V17.0.0
// This is here for backward compatibility in case the auth server
// does not support enriched resources yet.
if len(allowedLogins) == 0 {
logins, err := loginGetter.GetAllowedLoginsForResource(r)
if err != nil {
return nil, trace.Wrap(err)
}
slices.Sort(logins)
return logins, nil
}

localLogins := identity.Principals

allowed := make(map[string]struct{})
for _, login := range allowedLogins {
allowed[login] = struct{}{}
}

var logins []string
for _, local := range localLogins {
if _, ok := allowed[local]; ok {
logins = append(logins, local)
}
}

slices.Sort(logins)
return logins, nil
}

// calculateDesktopLogins determines the desktop logins allowed for the provided resource.
// If no logins are provided, then the checker is interrogated to determine which logins
// are allowed.
//
// TODO(tross) DELETE IN V17.0.0
// This is here for backward compatibility if the auth server doesn't yet support enriching
// resources with login information.
func calculateDesktopLogins(loginGetter loginGetter, r types.ResourceWithLabels, allowedLogins []string) ([]string, error) {
if len(allowedLogins) > 0 {
return allowedLogins, nil
}

logins, err := loginGetter.GetAllowedLoginsForResource(r)
return logins, trace.Wrap(err)
}

// clusterUnifiedResourcesGet returns a list of resources for a given cluster site. This includes all resources available to be displayed in the web ui
// such as Nodes, Apps, Desktops, etc etc
func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
Expand All @@ -2675,7 +2734,7 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt
return nil, trace.Wrap(err)
}

page, err := apiclient.ListUnifiedResourcePage(request.Context(), clt, req)
page, next, err := apiclient.GetUnifiedResourcePage(request.Context(), clt, req)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -2718,15 +2777,16 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt
var dbNames, dbUsers []string
hasFetchedDBUsersAndNames := false

unifiedResources := make([]any, 0, len(page.Resources))
for _, resource := range page.Resources {
switch r := resource.(type) {
unifiedResources := make([]any, 0, len(page))
for _, enriched := range page {
switch r := enriched.ResourceWithLabels.(type) {
case types.Server:
server, err := ui.MakeServer(site.GetName(), r, accessChecker)
logins, err := calculateSSHLogins(identity, accessChecker, r, enriched.Logins)
if err != nil {
return nil, trace.Wrap(err)
}
unifiedResources = append(unifiedResources, server)

unifiedResources = append(unifiedResources, ui.MakeServer(site.GetName(), r, logins))
case types.DatabaseServer:
if !hasFetchedDBUsersAndNames {
dbNames, dbUsers, err = getDatabaseUsersAndNames(accessChecker)
Expand Down Expand Up @@ -2779,26 +2839,26 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt
})
unifiedResources = append(unifiedResources, app)
case types.WindowsDesktop:
desktop, err := ui.MakeDesktop(r, accessChecker)
logins, err := calculateDesktopLogins(accessChecker, r, enriched.Logins)
if err != nil {
return nil, trace.Wrap(err)
}
unifiedResources = append(unifiedResources, desktop)

unifiedResources = append(unifiedResources, ui.MakeDesktop(r, logins))
case types.KubeCluster:
kube := ui.MakeKubeCluster(r, accessChecker)
unifiedResources = append(unifiedResources, kube)
case types.KubeServer:
kube := ui.MakeKubeCluster(r.GetCluster(), accessChecker)
unifiedResources = append(unifiedResources, kube)
default:
return nil, trace.Errorf("UI Resource has unknown type: %T", resource)
return nil, trace.Errorf("UI Resource has unknown type: %T", enriched)
}
}

resp := listResourcesGetResponse{
Items: unifiedResources,
StartKey: page.NextKey,
TotalCount: page.Total,
Items: unifiedResources,
StartKey: next,
}

return resp, nil
Expand All @@ -2817,22 +2877,38 @@ func (h *Handler) clusterNodesGet(w http.ResponseWriter, r *http.Request, p http
if err != nil {
return nil, trace.Wrap(err)
}
req.IncludeLogins = true

page, err := apiclient.GetResourcePage[types.Server](r.Context(), clt, req)
page, err := apiclient.GetEnrichedResourcePage(r.Context(), clt, req)
if err != nil {
return nil, trace.Wrap(err)
}

accessChecker, err := sctx.GetUserAccessChecker()
identity, err := sctx.GetIdentity()
if err != nil {
return nil, trace.Wrap(err)
}

uiServers, err := ui.MakeServers(site.GetName(), page.Resources, accessChecker)
accessChecker, err := sctx.GetUserAccessChecker()
if err != nil {
return nil, trace.Wrap(err)
}

uiServers := make([]ui.Server, 0, len(page.Resources))
for _, resource := range page.Resources {
server, ok := resource.ResourceWithLabels.(types.Server)
if !ok {
continue
}

logins, err := calculateSSHLogins(identity, accessChecker, server, resource.Logins)
if err != nil {
return nil, trace.Wrap(err)
}

uiServers = append(uiServers, ui.MakeServer(site.GetName(), server, logins))
}

return listResourcesGetResponse{
Items: uiServers,
StartKey: page.NextKey,
Expand Down
Loading