mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
Add LinuxDesktop resource to unified resources (#63101)
* Add LinuxDesktop gRPC and backend * Add LinuxDesktop resource to unified resources * Remove CloneResource * Review comments * Fix logins * Review comment * Add ClonableResourceWithLabels * Fix missing CloneResource * Update lib/auth/linuxdesktop/linuxdesktopv1/service.go Co-authored-by: Zac Bergquist <[email protected]> * Fix role * Update lib/auth/auth_with_roles.go Co-authored-by: Zac Bergquist <[email protected]> * Update lib/web/ui/server.go Co-authored-by: Zac Bergquist <[email protected]> * Fix compilation * e * skip gogoproto * e * fix test * Update api/client/proto/linuxdesktop.go Co-authored-by: Zac Bergquist <[email protected]> --------- Co-authored-by: Zac Bergquist <[email protected]>
This commit is contained in:
co-authored by
Zac Bergquist
parent
a13ff87916
commit
1c0a636206
@@ -4521,6 +4521,9 @@ func convertEnrichedResource(resource *proto.PaginatedResource) (*types.Enriched
|
||||
return &types.EnrichedResource{ResourceWithLabels: r, RequiresRequest: resource.RequiresRequest}, nil
|
||||
} else if r := resource.GetGitServer(); r != nil {
|
||||
return &types.EnrichedResource{ResourceWithLabels: r, RequiresRequest: resource.RequiresRequest}, nil
|
||||
} else if r := resource.GetLinuxDesktop(); r != nil {
|
||||
desktop := proto.UnpackLinuxDesktop(r)
|
||||
return &types.EnrichedResource{ResourceWithLabels: desktop, Logins: resource.Logins, RequiresRequest: resource.RequiresRequest}, nil
|
||||
} else {
|
||||
return nil, trace.BadParameter("received unsupported resource %T", resource.Resource)
|
||||
}
|
||||
|
||||
+1671
-1082
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 Gravitational, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
)
|
||||
|
||||
// PackLinuxDesktop packs Linux desktop into its wire format.
|
||||
func PackLinuxDesktop(desktop *linuxdesktopv1.LinuxDesktop) isPaginatedResource_Resource {
|
||||
return &PaginatedResource_LinuxDesktop{
|
||||
LinuxDesktop: &LinuxDesktop{
|
||||
Kind: desktop.Kind,
|
||||
SubKind: desktop.SubKind,
|
||||
Version: desktop.Version,
|
||||
Metadata: types.Metadata153ToLegacy(desktop.Metadata),
|
||||
Addr: desktop.Spec.Addr,
|
||||
Hostname: desktop.Spec.Hostname,
|
||||
ProxyIds: desktop.Spec.ProxyIds,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// UnpackLinuxDesktop converts a wire-format LinuxDesktop resource back into an linuxdesktopv1.LinuxDesktop instance.
|
||||
func UnpackLinuxDesktop(src *LinuxDesktop) types.ResourceWithLabels {
|
||||
dst := &linuxdesktopv1.LinuxDesktop{
|
||||
Kind: src.Kind,
|
||||
SubKind: src.SubKind,
|
||||
Version: src.Version,
|
||||
Metadata: types.LegacyTo153Metadata(src.Metadata),
|
||||
Spec: &linuxdesktopv1.LinuxDesktopSpec{
|
||||
Addr: src.Addr,
|
||||
Hostname: src.Hostname,
|
||||
ProxyIds: src.ProxyIds,
|
||||
},
|
||||
}
|
||||
return types.ProtoResource153ToLegacy(dst)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2026 Gravitational, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
|
||||
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
|
||||
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
)
|
||||
|
||||
func TestPackUnpackLinuxDesktop(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
desktop *linuxdesktopv1.LinuxDesktop
|
||||
}{
|
||||
{
|
||||
name: "basic linux desktop",
|
||||
desktop: &linuxdesktopv1.LinuxDesktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
SubKind: "",
|
||||
Version: types.V3,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "test-linux-desktop",
|
||||
Labels: map[string]string{
|
||||
"env": "production",
|
||||
"region": "us-east-1",
|
||||
},
|
||||
},
|
||||
Spec: &linuxdesktopv1.LinuxDesktopSpec{
|
||||
Addr: "192.168.1.100:22",
|
||||
Hostname: "linux-host-1",
|
||||
ProxyIds: []string{"proxy-1", "proxy-2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linux desktop with no labels",
|
||||
desktop: &linuxdesktopv1.LinuxDesktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
Version: types.V3,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "minimal-desktop",
|
||||
},
|
||||
Spec: &linuxdesktopv1.LinuxDesktopSpec{
|
||||
Addr: "example.com:22",
|
||||
Hostname: "minimal-host",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linux desktop with empty proxy IDs",
|
||||
desktop: &linuxdesktopv1.LinuxDesktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
Version: types.V3,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "no-proxy-desktop",
|
||||
Labels: map[string]string{
|
||||
"test": "label",
|
||||
},
|
||||
},
|
||||
Spec: &linuxdesktopv1.LinuxDesktopSpec{
|
||||
Addr: "172.16.0.10:22",
|
||||
Hostname: "isolated-host",
|
||||
ProxyIds: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Pack the desktop
|
||||
packed := PackLinuxDesktop(tt.desktop)
|
||||
require.NotNil(t, packed)
|
||||
|
||||
// Verify packed structure
|
||||
paginatedResource, ok := packed.(*PaginatedResource_LinuxDesktop)
|
||||
require.True(t, ok, "packed resource should be *PaginatedResource_LinuxDesktop")
|
||||
require.NotNil(t, paginatedResource.LinuxDesktop)
|
||||
|
||||
// Verify wire format fields
|
||||
wireDesktop := paginatedResource.LinuxDesktop
|
||||
require.Equal(t, tt.desktop.Kind, wireDesktop.Kind)
|
||||
require.Equal(t, tt.desktop.SubKind, wireDesktop.SubKind)
|
||||
require.Equal(t, tt.desktop.Version, wireDesktop.Version)
|
||||
require.Equal(t, tt.desktop.Metadata.Name, wireDesktop.Metadata.Name)
|
||||
require.Equal(t, tt.desktop.Metadata.Labels, wireDesktop.Metadata.Labels)
|
||||
require.Equal(t, tt.desktop.Spec.Addr, wireDesktop.Addr)
|
||||
require.Equal(t, tt.desktop.Spec.Hostname, wireDesktop.Hostname)
|
||||
require.Equal(t, tt.desktop.Spec.ProxyIds, wireDesktop.ProxyIds)
|
||||
|
||||
// Unpack the desktop
|
||||
unpacked := UnpackLinuxDesktop(wireDesktop)
|
||||
require.NotNil(t, unpacked)
|
||||
|
||||
// Unwrap to get the original proto type
|
||||
unwrapper, ok := unpacked.(types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop])
|
||||
require.True(t, ok, "unpacked resource should be unwrappable")
|
||||
|
||||
unpackedDesktop := unwrapper.UnwrapT()
|
||||
require.NotNil(t, unpackedDesktop)
|
||||
|
||||
// Verify round-trip equality using protocmp
|
||||
// Ignore the Expires field as it's added during metadata conversion
|
||||
if diff := cmp.Diff(tt.desktop, unpackedDesktop, protocmp.Transform(), protocmp.IgnoreFields(&headerv1.Metadata{}, "expires")); diff != "" {
|
||||
t.Errorf("round-trip mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2193,6 +2193,23 @@ message IdentityCenterAccountAssignment {
|
||||
IdentityCenterPermissionSet PermissionSet = 7;
|
||||
}
|
||||
|
||||
// LinuxDesktop represents Linux desktop. This is strictly a wire-format object
|
||||
// for use with the unified resource cache, and the types defined in the
|
||||
// `linuxdesktop` package should be used for actual processing.
|
||||
message LinuxDesktop {
|
||||
// Kind is the Linux desktop resource kind.
|
||||
string kind = 1;
|
||||
// SubKind is an optional resource subkind.
|
||||
string sub_kind = 2;
|
||||
// Version is the resource version.
|
||||
string version = 3;
|
||||
// Metadata is the account metadata.
|
||||
types.Metadata metadata = 4 [(gogoproto.nullable) = false];
|
||||
string addr = 5;
|
||||
string hostname = 6;
|
||||
repeated string proxy_ids = 7;
|
||||
}
|
||||
|
||||
message ListInstallersRequest {
|
||||
// The maximum number of items to return.
|
||||
// The server may impose a different page size at its discretion.
|
||||
@@ -2239,6 +2256,8 @@ message PaginatedResource {
|
||||
// IdentityCenterAccountAssignment represents a requestable Identity Center
|
||||
// Account Assignment
|
||||
IdentityCenterAccountAssignment IdentityCenterAccountAssignment = 16 [(gogoproto.jsontag) = "identity_center_account_assignment,omitempty"];
|
||||
// LinuxDesktop represents LinuxDesktop resource
|
||||
LinuxDesktop linux_desktop = 17;
|
||||
}
|
||||
|
||||
// Logins allowed for the included resource. Only to be populated for SSH and Desktops.
|
||||
|
||||
@@ -329,6 +329,11 @@ type protoResource153ToLegacyAdapter[T ProtoResource153] struct {
|
||||
resource153ToResourceWithLabelsAdapter[T]
|
||||
}
|
||||
|
||||
func (r *protoResource153ToLegacyAdapter[T]) CloneResource() ResourceWithLabels {
|
||||
clone := proto.CloneOf(r.inner)
|
||||
return ProtoResource153ToLegacy(clone)
|
||||
}
|
||||
|
||||
// UnwrapT is an escape hatch for Resource153 instances that are piped down into
|
||||
// the codebase as a legacy Resource.
|
||||
//
|
||||
@@ -352,7 +357,7 @@ func (r *protoResource153ToLegacyAdapter[T]) MarshalJSON() ([]byte, error) {
|
||||
//
|
||||
// Note that CheckAndSetDefaults is a noop for the returned resource and
|
||||
// SetSubKind is not implemented and panics on use.
|
||||
func ProtoResource153ToLegacy[T ProtoResource153](r T) ResourceWithLabels {
|
||||
func ProtoResource153ToLegacy[T ProtoResource153](r T) ClonableResourceWithLabels {
|
||||
return &protoResource153ToLegacyAdapter[T]{
|
||||
r,
|
||||
resource153ToResourceWithLabelsAdapter[T]{
|
||||
@@ -416,3 +421,8 @@ func ConvertResource[T any](resource Resource) (T, error) {
|
||||
var zero T
|
||||
return zero, trace.BadParameter("expected resource type %T, got %T", zero, resource)
|
||||
}
|
||||
|
||||
type ClonableResourceWithLabels interface {
|
||||
ResourceWithLabels
|
||||
CloneResource() ResourceWithLabels
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
auditlogpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/auditlog/v1"
|
||||
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
|
||||
identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
|
||||
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
|
||||
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
|
||||
"github.com/gravitational/teleport/api/internalutils/stream"
|
||||
"github.com/gravitational/teleport/api/metadata"
|
||||
@@ -1445,6 +1446,7 @@ var (
|
||||
types.KindNode: {},
|
||||
types.KindSAMLIdPServiceProvider: {},
|
||||
types.KindWindowsDesktop: {},
|
||||
types.KindLinuxDesktop: {},
|
||||
types.KindMCP: {},
|
||||
}
|
||||
|
||||
@@ -1622,6 +1624,17 @@ func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.L
|
||||
continue
|
||||
}
|
||||
r.Logins = logins
|
||||
} else if d := r.GetLinuxDesktop(); d != nil {
|
||||
desktop := proto.UnpackLinuxDesktop(d)
|
||||
logins, err := resourceLister.getAllowedLogins(desktop)
|
||||
if err != nil {
|
||||
a.authServer.logger.WarnContext(ctx, "Unable to determine logins for Linux desktop",
|
||||
"error", err,
|
||||
"resource", desktop.GetName(),
|
||||
)
|
||||
continue
|
||||
}
|
||||
r.Logins = logins
|
||||
} else if d := r.GetAppServer(); d != nil {
|
||||
// Apps representing an Identity Center Account have a collection of Permission Sets
|
||||
// that can be thought of as individually-addressable sub-resources. To present a consitent
|
||||
@@ -2359,6 +2372,11 @@ func (r *resourceChecker) CanAccess(resource types.ResourceWithLabels) error {
|
||||
if isCheckable {
|
||||
return r.CheckAccess(checkable, state, services.NewIdentityCenterAccountAssignmentMatcher(rr.UnwrapT()))
|
||||
}
|
||||
case types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop]:
|
||||
checkable, isCheckable := rr.(services.AccessCheckable)
|
||||
if isCheckable {
|
||||
return r.CheckAccess(checkable, state)
|
||||
}
|
||||
}
|
||||
|
||||
return trace.BadParameter("could not check access to resource type %T", resource)
|
||||
|
||||
@@ -240,14 +240,9 @@ func (s *Service) UpsertLinuxDesktop(ctx context.Context, req *linuxdesktopv1.Up
|
||||
if err := checkAccess(authCtx, existing); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := checkAccess(authCtx, desktop); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
} else {
|
||||
if err := checkAccess(authCtx, desktop); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
if err := checkAccess(authCtx, desktop); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
rsp, err := s.backend.UpsertLinuxDesktop(ctx, desktop)
|
||||
|
||||
@@ -35,7 +35,9 @@ import (
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
|
||||
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/utils/clientutils"
|
||||
"github.com/gravitational/teleport/lib/backend"
|
||||
"github.com/gravitational/teleport/lib/componentfeatures"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
@@ -396,6 +398,7 @@ func (c *UnifiedResourceCache) itemKindMatches(r resource, kinds map[string]stru
|
||||
switch r.GetKind() {
|
||||
case types.KindNode,
|
||||
types.KindWindowsDesktop,
|
||||
types.KindLinuxDesktop,
|
||||
types.KindGitServer,
|
||||
types.KindDatabase,
|
||||
types.KindKubernetesCluster:
|
||||
@@ -527,6 +530,7 @@ type ResourceGetter interface {
|
||||
DatabaseServersGetter
|
||||
AppServersGetter
|
||||
WindowsDesktopGetter
|
||||
LinuxDesktopGetter
|
||||
KubernetesServerGetter
|
||||
SAMLIdpServiceProviderGetter
|
||||
IdentityCenterAccountGetter
|
||||
@@ -641,6 +645,11 @@ func (c *UnifiedResourceCache) getResourcesAndUpdateCurrent(ctx context.Context)
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
newLinuxDesktops, err := c.getLinuxDesktops(ctx)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
newICAccounts, err := c.getIdentityCenterAccounts(ctx)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
@@ -666,6 +675,7 @@ func (c *UnifiedResourceCache) getResourcesAndUpdateCurrent(ctx context.Context)
|
||||
putResources(c, newKubes)
|
||||
putResources(c, newSAMLApps)
|
||||
putResources(c, newDesktops)
|
||||
putResources(c, newLinuxDesktops)
|
||||
putResources(c, newICAccounts)
|
||||
putResources(c, newGitServers)
|
||||
c.stale = false
|
||||
@@ -720,6 +730,18 @@ func (c *UnifiedResourceCache) getDesktops(ctx context.Context) ([]types.Windows
|
||||
return newDesktops, nil
|
||||
}
|
||||
|
||||
// getLinuxDesktops will get all Linux desktops
|
||||
func (c *UnifiedResourceCache) getLinuxDesktops(ctx context.Context) ([]resource, error) {
|
||||
var linuxDesktops []resource
|
||||
for linuxDesktop, err := range clientutils.Resources(ctx, c.ListLinuxDesktops) {
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
linuxDesktops = append(linuxDesktops, types.ProtoResource153ToLegacy(linuxDesktop))
|
||||
}
|
||||
return linuxDesktops, nil
|
||||
}
|
||||
|
||||
// getSAMLApps will get all SAML Idp Service Providers
|
||||
func (c *UnifiedResourceCache) getSAMLApps(ctx context.Context) ([]types.SAMLIdPServiceProvider, error) {
|
||||
var newSAMLApps []types.SAMLIdPServiceProvider
|
||||
@@ -905,6 +927,7 @@ func (c *UnifiedResourceCache) resourceKinds() []types.WatchKind {
|
||||
{Kind: types.KindDatabaseServer},
|
||||
{Kind: types.KindAppServer},
|
||||
{Kind: types.KindWindowsDesktop},
|
||||
{Kind: types.KindLinuxDesktop},
|
||||
{Kind: types.KindSAMLIdPServiceProvider},
|
||||
{Kind: types.KindIdentityCenterAccount},
|
||||
{Kind: types.KindGitServer},
|
||||
@@ -1234,6 +1257,13 @@ func MakePaginatedResource(requestType string, r types.ResourceWithLabels, requi
|
||||
}
|
||||
|
||||
protoResource = &proto.PaginatedResource{Resource: &proto.PaginatedResource_KubernetesServer{KubernetesServer: srv}, RequiresRequest: requiresRequest}
|
||||
case types.KindLinuxDesktop:
|
||||
unwrapper, ok := resource.(types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop])
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("%s has invalid type %T", resourceKind, resource)
|
||||
}
|
||||
|
||||
protoResource = &proto.PaginatedResource{Resource: proto.PackLinuxDesktop(unwrapper.UnwrapT()), Logins: logins, RequiresRequest: requiresRequest}
|
||||
case types.KindWindowsDesktop:
|
||||
desktop, ok := resource.(*types.WindowsDesktopV3)
|
||||
if !ok {
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
|
||||
identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
|
||||
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/types/common"
|
||||
"github.com/gravitational/teleport/api/types/header"
|
||||
@@ -54,6 +55,7 @@ type client struct {
|
||||
bk backend.Backend
|
||||
services.Presence
|
||||
services.WindowsDesktops
|
||||
services.LinuxDesktops
|
||||
services.SAMLIdPServiceProviders
|
||||
services.GitServers
|
||||
services.IdentityCenterAccounts
|
||||
@@ -74,11 +76,14 @@ func newClient(t *testing.T) *client {
|
||||
Backend: bk,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
linuxDesktopService, err := local.NewLinuxDesktopService(bk)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &client{
|
||||
bk: bk,
|
||||
Presence: local.NewPresenceService(bk),
|
||||
WindowsDesktops: local.NewWindowsDesktopService(bk),
|
||||
LinuxDesktops: linuxDesktopService,
|
||||
SAMLIdPServiceProviders: samlService,
|
||||
Events: local.NewEventsService(bk),
|
||||
GitServers: gitService,
|
||||
@@ -89,7 +94,7 @@ func newClient(t *testing.T) *client {
|
||||
func TestUnifiedResourceWatcher(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
clt := newClient(t)
|
||||
|
||||
// Add node to the backend.
|
||||
@@ -185,9 +190,10 @@ func TestUnifiedResourceWatcher(t *testing.T) {
|
||||
gitServer, gitServer2,
|
||||
services.IdentityCenterAccountToAppServer(icAcct),
|
||||
}
|
||||
assert.Eventually(t, func() bool {
|
||||
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
res, err = w.GetUnifiedResources(ctx)
|
||||
return len(res) == len(expectedRes)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, len(expectedRes))
|
||||
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for unified resources to be added")
|
||||
assert.Empty(t, cmp.Diff(
|
||||
expectedRes,
|
||||
@@ -213,14 +219,15 @@ func TestUnifiedResourceWatcher(t *testing.T) {
|
||||
services.IdentityCenterAccountToAppServer(icAcct),
|
||||
}
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
res, err = w.GetUnifiedResources(ctx)
|
||||
require.NoError(t, err)
|
||||
serverUpdated := slices.ContainsFunc(res, func(r types.ResourceWithLabels) bool {
|
||||
node, ok := r.(types.Server)
|
||||
return ok && node.GetAddr() == "192.168.0.1:22"
|
||||
})
|
||||
return len(res) == len(expectedRes) && serverUpdated
|
||||
require.True(t, serverUpdated)
|
||||
require.Len(t, res, len(expectedRes))
|
||||
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for unified resources to be updated")
|
||||
assert.Empty(t, cmp.Diff(
|
||||
expectedRes,
|
||||
@@ -238,7 +245,7 @@ func TestUnifiedResourceWatcher(t *testing.T) {
|
||||
func TestUnifiedResourceCacheIterateResources(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
clt := newClient(t)
|
||||
|
||||
node := newNodeServer(t, "node1", "hostname1", "127.0.0.1:22", false /*tunnel*/)
|
||||
@@ -425,7 +432,7 @@ func TestUnifiedResourceCacheIterateResources(t *testing.T) {
|
||||
func TestUnifiedResourceCacheIteration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
const resourceCount = 1234
|
||||
ids := make([]string, 0, resourceCount)
|
||||
@@ -820,7 +827,7 @@ func TestUnifiedResourceCacheIteration(t *testing.T) {
|
||||
func TestUnifiedResourceWatcher_PreventDuplicates(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
clt := newClient(t)
|
||||
w, err := services.NewUnifiedResourceCache(ctx, services.UnifiedResourceCacheConfig{
|
||||
ResourceWatcherConfig: services.ResourceWatcherConfig{
|
||||
@@ -836,9 +843,10 @@ func TestUnifiedResourceWatcher_PreventDuplicates(t *testing.T) {
|
||||
_, err = clt.UpsertNode(ctx, node)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
res, _ := w.GetUnifiedResources(ctx)
|
||||
return len(res) == 1
|
||||
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
res, err := w.GetUnifiedResources(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for unified resources to be added")
|
||||
|
||||
// update a node
|
||||
@@ -846,10 +854,11 @@ func TestUnifiedResourceWatcher_PreventDuplicates(t *testing.T) {
|
||||
_, err = clt.UpsertNode(ctx, updatedNode)
|
||||
require.NoError(t, err)
|
||||
|
||||
// only one resource should still exists with the name "node1" (with hostname updated)
|
||||
assert.Eventually(t, func() bool {
|
||||
res, _ := w.GetUnifiedResources(ctx)
|
||||
return len(res) == 1
|
||||
// only one resource should still exist with the name "node1" (with hostname updated)
|
||||
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
res, err := w.GetUnifiedResources(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for unified resources to be added")
|
||||
|
||||
}
|
||||
@@ -975,7 +984,7 @@ func TestUnifiedResourceCache_AppServerComponentFeaturesIntersection(t *testing.
|
||||
func TestUnifiedResourceWatcher_DeleteEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
clt := newClient(t)
|
||||
w, err := services.NewUnifiedResourceCache(ctx, services.UnifiedResourceCacheConfig{
|
||||
ResourceWatcherConfig: services.ResourceWatcherConfig{
|
||||
@@ -1094,9 +1103,10 @@ func TestUnifiedResourceWatcher_DeleteEvent(t *testing.T) {
|
||||
_, err = clt.CreateGitServer(ctx, gitServer)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
res, _ := w.GetUnifiedResources(ctx)
|
||||
return len(res) == 8
|
||||
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
res, err := w.GetUnifiedResources(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 8)
|
||||
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for unified resources to be added")
|
||||
|
||||
// delete just one of each of the HA servers
|
||||
@@ -1222,7 +1232,7 @@ func newICAccount(t *testing.T, ctx context.Context, svc services.IdentityCenter
|
||||
|
||||
func TestOktaAppServers(t *testing.T) {
|
||||
clt := newClient(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
appsServer := []*types.AppServerV3{
|
||||
mustCreateOktaAppServer(t, uuid.NewString(), "App 1"),
|
||||
@@ -1234,7 +1244,7 @@ func TestOktaAppServers(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
w, err := services.NewUnifiedResourceCache(context.Background(), services.UnifiedResourceCacheConfig{
|
||||
w, err := services.NewUnifiedResourceCache(t.Context(), services.UnifiedResourceCacheConfig{
|
||||
ResourceWatcherConfig: services.ResourceWatcherConfig{
|
||||
Component: teleport.ComponentUnifiedResource,
|
||||
Client: clt,
|
||||
@@ -1309,7 +1319,7 @@ func newMCPServerApp(t *testing.T, name string) *types.AppV3 {
|
||||
func TestUnifiedResourceCacheIterateMCPServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
clt := newClient(t)
|
||||
|
||||
app, err := types.NewAppServerV3(
|
||||
@@ -1414,3 +1424,290 @@ func TestUnifiedResourceCacheIterateMCPServers(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnifiedResourceLinuxDesktop(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := t.Context()
|
||||
clt := newClient(t)
|
||||
|
||||
// Create a unified resource cache
|
||||
w, err := services.NewUnifiedResourceCache(ctx, services.UnifiedResourceCacheConfig{
|
||||
ResourceWatcherConfig: services.ResourceWatcherConfig{
|
||||
Client: clt,
|
||||
Component: teleport.ComponentUnifiedResource,
|
||||
},
|
||||
ResourceGetter: clt,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create and upsert a Linux desktop
|
||||
linuxDesktop1 := &linuxdesktopv1.LinuxDesktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
Version: types.V3,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "linux-desktop-1",
|
||||
Labels: map[string]string{
|
||||
"env": "production",
|
||||
"region": "us-west-1",
|
||||
},
|
||||
},
|
||||
Spec: &linuxdesktopv1.LinuxDesktopSpec{
|
||||
Addr: "10.0.0.1:22",
|
||||
Hostname: "linux-host-1",
|
||||
ProxyIds: []string{"proxy-1"},
|
||||
},
|
||||
}
|
||||
_, err = clt.UpsertLinuxDesktop(ctx, linuxDesktop1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the resource to appear in the cache
|
||||
var resources []types.ResourceWithLabels
|
||||
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
resources, err = w.GetUnifiedResources(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resources, 1)
|
||||
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for Linux desktop to be added")
|
||||
|
||||
// Verify the resource is a Linux desktop with correct properties
|
||||
require.Len(t, resources, 1)
|
||||
resource := resources[0]
|
||||
require.Equal(t, types.KindLinuxDesktop, resource.GetKind())
|
||||
require.Equal(t, "linux-desktop-1", resource.GetName())
|
||||
require.Contains(t, resource.GetAllLabels(), "env")
|
||||
require.Equal(t, "production", resource.GetAllLabels()["env"])
|
||||
|
||||
// Verify it can be unwrapped
|
||||
unwrapper, ok := resource.(types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop])
|
||||
require.True(t, ok, "resource should be unwrappable to LinuxDesktop")
|
||||
unwrapped := unwrapper.UnwrapT()
|
||||
require.Equal(t, "linux-desktop-1", unwrapped.Metadata.Name)
|
||||
require.Equal(t, "10.0.0.1:22", unwrapped.Spec.Addr)
|
||||
require.Equal(t, "linux-host-1", unwrapped.Spec.Hostname)
|
||||
|
||||
// Add a second Linux desktop
|
||||
linuxDesktop2 := &linuxdesktopv1.LinuxDesktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
Version: types.V3,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "linux-desktop-2",
|
||||
Labels: map[string]string{
|
||||
"env": "staging",
|
||||
},
|
||||
},
|
||||
Spec: &linuxdesktopv1.LinuxDesktopSpec{
|
||||
Addr: "10.0.0.2:22",
|
||||
Hostname: "linux-host-2",
|
||||
ProxyIds: []string{"proxy-2"},
|
||||
},
|
||||
}
|
||||
_, err = clt.UpsertLinuxDesktop(ctx, linuxDesktop2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for both resources to appear
|
||||
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
resources, err = w.GetUnifiedResources(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resources, 2)
|
||||
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for second Linux desktop")
|
||||
|
||||
// Verify both desktops are present
|
||||
require.Len(t, resources, 2)
|
||||
names := []string{resources[0].GetName(), resources[1].GetName()}
|
||||
require.ElementsMatch(t, []string{"linux-desktop-1", "linux-desktop-2"}, names)
|
||||
|
||||
// Delete the first desktop
|
||||
err = clt.DeleteLinuxDesktop(ctx, "linux-desktop-1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for deletion to be reflected
|
||||
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
resources, err = w.GetUnifiedResources(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resources, 1)
|
||||
require.Equal(t, "linux-desktop-2", resources[0].GetName())
|
||||
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for Linux desktop deletion")
|
||||
}
|
||||
|
||||
func TestUnifiedResourceLinuxDesktopFiltering(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := t.Context()
|
||||
clt := newClient(t)
|
||||
|
||||
// Create a unified resource cache
|
||||
w, err := services.NewUnifiedResourceCache(ctx, services.UnifiedResourceCacheConfig{
|
||||
ResourceWatcherConfig: services.ResourceWatcherConfig{
|
||||
Client: clt,
|
||||
Component: teleport.ComponentUnifiedResource,
|
||||
},
|
||||
ResourceGetter: clt,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for cache initialization to avoid test flakiness
|
||||
assert.Eventually(t, w.IsInitialized, 5*time.Second, 10*time.Millisecond, "Timed out waiting for all resources cache initialization")
|
||||
|
||||
// Add a Linux desktop
|
||||
linuxDesktop := &linuxdesktopv1.LinuxDesktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
Version: types.V3,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "linux-desktop",
|
||||
Labels: map[string]string{
|
||||
"env": "test",
|
||||
},
|
||||
},
|
||||
Spec: &linuxdesktopv1.LinuxDesktopSpec{
|
||||
Addr: "10.0.0.10:22",
|
||||
Hostname: "test-host",
|
||||
},
|
||||
}
|
||||
_, err = clt.UpsertLinuxDesktop(ctx, linuxDesktop)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add a Windows desktop for comparison
|
||||
windowsDesktop, err := types.NewWindowsDesktopV3(
|
||||
"windows-desktop",
|
||||
map[string]string{"env": "test"},
|
||||
types.WindowsDesktopSpecV3{
|
||||
Addr: "10.0.0.20:3389",
|
||||
HostID: "windows-host",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = clt.UpsertWindowsDesktop(ctx, windowsDesktop)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add a node for comparison
|
||||
node := newNodeServer(t, "node", "node-host", "10.0.0.30:22", false)
|
||||
_, err = clt.UpsertNode(ctx, node)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for all resources
|
||||
var allResources []types.ResourceWithLabels
|
||||
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
allResources, err = w.GetUnifiedResources(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allResources, 3)
|
||||
}, 5*time.Second, 10*time.Millisecond, "Timed out waiting for all resources")
|
||||
|
||||
// Helper to collect resources from iterator
|
||||
collect := func(t *testing.T, iter iter.Seq2[types.ResourceWithLabels, error]) (collected []types.ResourceWithLabels) {
|
||||
for r, err := range iter {
|
||||
require.NoError(t, err)
|
||||
collected = append(collected, r)
|
||||
}
|
||||
return collected
|
||||
}
|
||||
|
||||
// Test filtering by Linux desktop kind only
|
||||
linuxOnly := collect(t, w.Resources(ctx, "", types.SortBy{}, types.KindLinuxDesktop))
|
||||
require.Len(t, linuxOnly, 1)
|
||||
require.Equal(t, types.KindLinuxDesktop, linuxOnly[0].GetKind())
|
||||
require.Equal(t, "linux-desktop", linuxOnly[0].GetName())
|
||||
|
||||
// Test filtering by Windows desktop kind only
|
||||
windowsOnly := collect(t, w.Resources(ctx, "", types.SortBy{}, types.KindWindowsDesktop))
|
||||
require.Len(t, windowsOnly, 1)
|
||||
require.Equal(t, types.KindWindowsDesktop, windowsOnly[0].GetKind())
|
||||
require.Equal(t, "windows-desktop", windowsOnly[0].GetName())
|
||||
|
||||
// Test filtering by both desktop kinds
|
||||
bothDesktops := collect(t, w.Resources(ctx, "", types.SortBy{}, types.KindLinuxDesktop, types.KindWindowsDesktop))
|
||||
require.Len(t, bothDesktops, 2)
|
||||
kinds := []string{bothDesktops[0].GetKind(), bothDesktops[1].GetKind()}
|
||||
require.ElementsMatch(t, []string{types.KindLinuxDesktop, types.KindWindowsDesktop}, kinds)
|
||||
|
||||
// Test filtering by node kind (should not include desktops)
|
||||
nodesOnly := collect(t, w.Resources(ctx, "", types.SortBy{}, types.KindNode))
|
||||
require.Len(t, nodesOnly, 1)
|
||||
require.Equal(t, types.KindNode, nodesOnly[0].GetKind())
|
||||
}
|
||||
|
||||
func TestMakePaginatedResourceLinuxDesktop(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
desktop *linuxdesktopv1.LinuxDesktop
|
||||
requiresRequest bool
|
||||
logins []string
|
||||
}{
|
||||
{
|
||||
name: "basic linux desktop",
|
||||
desktop: &linuxdesktopv1.LinuxDesktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
Version: types.V3,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "test-desktop",
|
||||
Labels: map[string]string{
|
||||
"env": "production",
|
||||
},
|
||||
},
|
||||
Spec: &linuxdesktopv1.LinuxDesktopSpec{
|
||||
Addr: "192.168.1.100:22",
|
||||
Hostname: "desktop-host",
|
||||
ProxyIds: []string{"proxy-1"},
|
||||
},
|
||||
},
|
||||
requiresRequest: false,
|
||||
logins: []string{"ubuntu", "root"},
|
||||
},
|
||||
{
|
||||
name: "linux desktop requiring request",
|
||||
desktop: &linuxdesktopv1.LinuxDesktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
Version: types.V3,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "protected-desktop",
|
||||
},
|
||||
Spec: &linuxdesktopv1.LinuxDesktopSpec{
|
||||
Addr: "10.0.0.50:22",
|
||||
Hostname: "protected-host",
|
||||
},
|
||||
},
|
||||
requiresRequest: true,
|
||||
logins: []string{"admin"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Convert to ResourceWithLabels
|
||||
wrapped := types.ProtoResource153ToLegacy(tt.desktop)
|
||||
|
||||
// Create paginated resource
|
||||
paginated, err := services.MakePaginatedResource(types.KindLinuxDesktop, wrapped, tt.requiresRequest)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, paginated)
|
||||
|
||||
// Verify the paginated resource contains a LinuxDesktop
|
||||
wireDesktop := paginated.GetLinuxDesktop()
|
||||
require.NotNil(t, wireDesktop, "paginated resource should contain LinuxDesktop")
|
||||
|
||||
// Verify fields match
|
||||
require.Equal(t, tt.desktop.Kind, wireDesktop.Kind)
|
||||
require.Equal(t, tt.desktop.Version, wireDesktop.Version)
|
||||
require.Equal(t, tt.desktop.Metadata.Name, wireDesktop.Metadata.Name)
|
||||
require.Equal(t, tt.desktop.Spec.Addr, wireDesktop.Addr)
|
||||
require.Equal(t, tt.desktop.Spec.Hostname, wireDesktop.Hostname)
|
||||
require.Equal(t, tt.desktop.Spec.ProxyIds, wireDesktop.ProxyIds)
|
||||
require.Equal(t, tt.requiresRequest, paginated.RequiresRequest)
|
||||
|
||||
// Test unpacking
|
||||
unpacked := proto.UnpackLinuxDesktop(wireDesktop)
|
||||
require.NotNil(t, unpacked)
|
||||
require.Equal(t, types.KindLinuxDesktop, unpacked.GetKind())
|
||||
require.Equal(t, tt.desktop.Metadata.Name, unpacked.GetName())
|
||||
|
||||
// Verify it can be unwrapped back to the original type
|
||||
unwrapper, ok := unpacked.(types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop])
|
||||
require.True(t, ok, "unpacked resource should be unwrappable")
|
||||
unpackedDesktop := unwrapper.UnwrapT()
|
||||
require.Equal(t, tt.desktop.Metadata.Name, unpackedDesktop.Metadata.Name)
|
||||
require.Equal(t, tt.desktop.Spec.Addr, unpackedDesktop.Spec.Addr)
|
||||
require.Equal(t, tt.desktop.Spec.Hostname, unpackedDesktop.Spec.Hostname)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/client/webclient"
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
|
||||
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
|
||||
notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1"
|
||||
summarizerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/summarizer/v1"
|
||||
@@ -3428,6 +3429,7 @@ func makeUnifiedResourceRequest(r *http.Request) (*proto.ListUnifiedResourcesReq
|
||||
types.KindDatabase,
|
||||
types.KindNode,
|
||||
types.KindWindowsDesktop,
|
||||
types.KindLinuxDesktop,
|
||||
types.KindKubernetesCluster,
|
||||
types.KindSAMLIdPServiceProvider,
|
||||
types.KindGitServer,
|
||||
@@ -3587,7 +3589,9 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt
|
||||
})
|
||||
unifiedResources = append(unifiedResources, app)
|
||||
case types.WindowsDesktop:
|
||||
unifiedResources = append(unifiedResources, ui.MakeDesktop(r, enriched.Logins, enriched.RequiresRequest))
|
||||
unifiedResources = append(unifiedResources, ui.MakeWindowsDesktop(r, enriched.Logins, enriched.RequiresRequest))
|
||||
case types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop]:
|
||||
unifiedResources = append(unifiedResources, ui.MakeLinuxDesktop(r.UnwrapT(), enriched.Logins, enriched.RequiresRequest))
|
||||
case types.KubeCluster:
|
||||
kube := ui.MakeKubeCluster(r, accessChecker, enriched.RequiresRequest)
|
||||
unifiedResources = append(unifiedResources, kube)
|
||||
|
||||
+7
-6
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"github.com/gravitational/teleport/api/client"
|
||||
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/httplib"
|
||||
"github.com/gravitational/teleport/lib/reversetunnelclient"
|
||||
@@ -260,12 +261,12 @@ func (h *Handler) clusterDesktopsGet(w http.ResponseWriter, r *http.Request, p h
|
||||
|
||||
uiDesktops := make([]webui.Desktop, 0, len(page.Resources))
|
||||
for _, r := range page.Resources {
|
||||
desktop, ok := r.ResourceWithLabels.(types.WindowsDesktop)
|
||||
if !ok {
|
||||
continue
|
||||
switch desktop := r.ResourceWithLabels.(type) {
|
||||
case types.WindowsDesktop:
|
||||
uiDesktops = append(uiDesktops, webui.MakeWindowsDesktop(desktop, r.Logins, false /* requiresRequest */))
|
||||
case types.Resource153UnwrapperT[*linuxdesktopv1.LinuxDesktop]:
|
||||
uiDesktops = append(uiDesktops, webui.MakeLinuxDesktop(desktop.UnwrapT(), r.Logins, false /* requiresRequest */))
|
||||
}
|
||||
|
||||
uiDesktops = append(uiDesktops, webui.MakeDesktop(desktop, r.Logins, false /* requiresRequest */))
|
||||
}
|
||||
|
||||
return listResourcesGetResponse{
|
||||
@@ -333,7 +334,7 @@ func (h *Handler) getDesktopHandle(w http.ResponseWriter, r *http.Request, p htt
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return webui.MakeDesktop(desktop, logins, false /* requiresRequest */), nil
|
||||
return webui.MakeWindowsDesktop(desktop, logins, false /* requiresRequest */), nil
|
||||
}
|
||||
|
||||
// desktopIsActive checks if a desktop has an active session and returns a desktopIsActive.
|
||||
|
||||
+21
-5
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
integrationv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1"
|
||||
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
@@ -524,10 +525,25 @@ type Desktop struct {
|
||||
RequiresRequest bool `json:"requiresRequest,omitempty"`
|
||||
}
|
||||
|
||||
// MakeDesktop converts a desktop from its API form to a type the UI can display.
|
||||
func MakeDesktop(windowsDesktop types.WindowsDesktop, logins []string, requiresRequest bool) Desktop {
|
||||
// stripRdpPort strips the default rdp port from an ip address since it is unimportant to display
|
||||
stripRdpPort := func(addr string) string {
|
||||
func MakeLinuxDesktop(linuxDesktop *linuxdesktopv1.LinuxDesktop, logins []string, requiresRequest bool) Desktop {
|
||||
uiLabels := ui.MakeLabelsWithoutInternalPrefixes(linuxDesktop.Metadata.Labels)
|
||||
|
||||
return Desktop{
|
||||
Kind: linuxDesktop.GetKind(),
|
||||
OS: constants.LinuxOS,
|
||||
Name: linuxDesktop.Spec.Hostname,
|
||||
Addr: linuxDesktop.Spec.Addr,
|
||||
Labels: uiLabels,
|
||||
HostID: linuxDesktop.Metadata.Name,
|
||||
Logins: logins,
|
||||
RequiresRequest: requiresRequest,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeWindowsDesktop converts a desktop from its API form to a type the UI can display.
|
||||
func MakeWindowsDesktop(windowsDesktop types.WindowsDesktop, logins []string, requiresRequest bool) Desktop {
|
||||
// stripRDPPort strips the default rdp port from an ip address since it is unimportant to display
|
||||
stripRDPPort := func(addr string) string {
|
||||
splitAddr := strings.Split(addr, ":")
|
||||
if len(splitAddr) > 1 && splitAddr[1] == strconv.Itoa(defaults.RDPListenPort) {
|
||||
return splitAddr[0]
|
||||
@@ -541,7 +557,7 @@ func MakeDesktop(windowsDesktop types.WindowsDesktop, logins []string, requiresR
|
||||
Kind: windowsDesktop.GetKind(),
|
||||
OS: constants.WindowsOS,
|
||||
Name: windowsDesktop.GetName(),
|
||||
Addr: stripRdpPort(windowsDesktop.GetAddr()),
|
||||
Addr: stripRDPPort(windowsDesktop.GetAddr()),
|
||||
Labels: uiLabels,
|
||||
HostID: windowsDesktop.GetHostID(),
|
||||
Logins: logins,
|
||||
|
||||
+130
-1
@@ -25,6 +25,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
|
||||
linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/componentfeatures"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
@@ -456,7 +458,7 @@ func TestMakeDesktopHiddenLabel(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
desktop := MakeDesktop(windowsDesktop, nil, false)
|
||||
desktop := MakeWindowsDesktop(windowsDesktop, nil, false)
|
||||
labels := []ui.Label{
|
||||
{
|
||||
Name: "label3",
|
||||
@@ -467,6 +469,133 @@ func TestMakeDesktopHiddenLabel(t *testing.T) {
|
||||
require.Equal(t, labels, desktop.Labels)
|
||||
}
|
||||
|
||||
func TestMakeLinuxDesktop(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
desktop func(t *testing.T) *linuxdesktopv1.LinuxDesktop
|
||||
logins []string
|
||||
requiresRequest bool
|
||||
expected Desktop
|
||||
}{
|
||||
{
|
||||
name: "basic linux desktop",
|
||||
desktop: func(t *testing.T) *linuxdesktopv1.LinuxDesktop {
|
||||
return &linuxdesktopv1.LinuxDesktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
Version: types.V3,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "linux-desktop-1",
|
||||
Labels: map[string]string{
|
||||
"env": "prod",
|
||||
"region": "us-west",
|
||||
},
|
||||
},
|
||||
Spec: &linuxdesktopv1.LinuxDesktopSpec{
|
||||
Addr: "10.0.0.1:22",
|
||||
Hostname: "linux-host-1",
|
||||
ProxyIds: []string{"proxy-1"},
|
||||
},
|
||||
}
|
||||
},
|
||||
logins: []string{"ubuntu", "root"},
|
||||
requiresRequest: false,
|
||||
expected: Desktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
OS: "linux",
|
||||
Name: "linux-host-1",
|
||||
Addr: "10.0.0.1:22",
|
||||
HostID: "linux-desktop-1",
|
||||
Labels: []ui.Label{
|
||||
{Name: "env", Value: "prod"},
|
||||
{Name: "region", Value: "us-west"},
|
||||
},
|
||||
Logins: []string{"ubuntu", "root"},
|
||||
RequiresRequest: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linux desktop with internal labels filtered",
|
||||
desktop: func(t *testing.T) *linuxdesktopv1.LinuxDesktop {
|
||||
return &linuxdesktopv1.LinuxDesktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
Version: types.V3,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "linux-desktop-2",
|
||||
Labels: map[string]string{
|
||||
"teleport.internal/resource-id": "12345",
|
||||
"teleport.hidden/secret": "value",
|
||||
"visible": "label",
|
||||
},
|
||||
},
|
||||
Spec: &linuxdesktopv1.LinuxDesktopSpec{
|
||||
Addr: "192.168.1.100:22",
|
||||
Hostname: "linux-host-2",
|
||||
},
|
||||
}
|
||||
},
|
||||
logins: []string{"admin"},
|
||||
requiresRequest: true,
|
||||
expected: Desktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
OS: "linux",
|
||||
Name: "linux-host-2",
|
||||
Addr: "192.168.1.100:22",
|
||||
HostID: "linux-desktop-2",
|
||||
Labels: []ui.Label{
|
||||
{Name: "visible", Value: "label"},
|
||||
},
|
||||
Logins: []string{"admin"},
|
||||
RequiresRequest: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linux desktop with no labels",
|
||||
desktop: func(t *testing.T) *linuxdesktopv1.LinuxDesktop {
|
||||
return &linuxdesktopv1.LinuxDesktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
Version: types.V3,
|
||||
Metadata: &headerv1.Metadata{
|
||||
Name: "linux-desktop-3",
|
||||
},
|
||||
Spec: &linuxdesktopv1.LinuxDesktopSpec{
|
||||
Addr: "example.com:2222",
|
||||
Hostname: "linux-host-3",
|
||||
},
|
||||
}
|
||||
},
|
||||
logins: nil,
|
||||
requiresRequest: false,
|
||||
expected: Desktop{
|
||||
Kind: types.KindLinuxDesktop,
|
||||
OS: "linux",
|
||||
Name: "linux-host-3",
|
||||
Addr: "example.com:2222",
|
||||
HostID: "linux-desktop-3",
|
||||
Labels: []ui.Label{},
|
||||
Logins: nil,
|
||||
RequiresRequest: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
desktop := MakeLinuxDesktop(tt.desktop(t), tt.logins, tt.requiresRequest)
|
||||
|
||||
require.Equal(t, tt.expected.Kind, desktop.Kind)
|
||||
require.Equal(t, tt.expected.OS, desktop.OS)
|
||||
require.Equal(t, tt.expected.Name, desktop.Name)
|
||||
require.Equal(t, tt.expected.Addr, desktop.Addr)
|
||||
require.Equal(t, tt.expected.HostID, desktop.HostID)
|
||||
require.Equal(t, tt.expected.Logins, desktop.Logins)
|
||||
require.Equal(t, tt.expected.RequiresRequest, desktop.RequiresRequest)
|
||||
require.ElementsMatch(t, tt.expected.Labels, desktop.Labels)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeDesktopServiceHiddenLabel(t *testing.T) {
|
||||
windowsDesktopService := &types.WindowsDesktopServiceV3{
|
||||
ResourceHeader: types.ResourceHeader{
|
||||
|
||||
+2
@@ -1150,6 +1150,8 @@ function getPrettyResourceKind(kind: RequestableResourceKind): string {
|
||||
return 'User Group';
|
||||
case 'windows_desktop':
|
||||
return 'Desktop';
|
||||
case 'linux_desktop':
|
||||
return 'Desktop';
|
||||
case 'saml_idp_service_provider':
|
||||
return 'SAML Application';
|
||||
case 'namespace':
|
||||
|
||||
@@ -44,6 +44,7 @@ export function getEmptyResourceState(): ResourceMap {
|
||||
kube_cluster: {},
|
||||
user_group: {},
|
||||
windows_desktop: {},
|
||||
linux_desktop: {},
|
||||
role: {},
|
||||
saml_idp_service_provider: {},
|
||||
namespace: {},
|
||||
|
||||
@@ -184,6 +184,8 @@ function getIcon(item: RequestableResourceKind): ComponentType<IconProps> {
|
||||
return Icon.GitHub;
|
||||
case 'windows_desktop':
|
||||
return Icon.Desktop;
|
||||
case 'linux_desktop':
|
||||
return Icon.Linux;
|
||||
case 'resource': // This probably never shows in the UI.
|
||||
return Icon.Server;
|
||||
default:
|
||||
|
||||
@@ -150,6 +150,7 @@ const filterKindNameMap: Record<ResourceFilterKind, string> = {
|
||||
app: 'Applications',
|
||||
db: 'Databases',
|
||||
windows_desktop: 'Desktops',
|
||||
linux_desktop: 'Desktops',
|
||||
kube_cluster: 'Kubernetes Clusters',
|
||||
node: 'SSH Resources',
|
||||
user_group: 'User Groups',
|
||||
|
||||
@@ -175,6 +175,28 @@ export function makeUnifiedResourceViewItemDesktop(
|
||||
};
|
||||
}
|
||||
|
||||
export function makeUnifiedResourceViewItemLinuxDesktop(
|
||||
resource: UnifiedResourceDesktop,
|
||||
ui: UnifiedResourceUi
|
||||
): UnifiedResourceViewItem {
|
||||
return {
|
||||
name: resource.name,
|
||||
SecondaryIcon: DesktopIcon,
|
||||
primaryIconName: 'linux',
|
||||
ActionButton: ui.ActionButton,
|
||||
labels: resource.labels,
|
||||
cardViewProps: {
|
||||
primaryDesc: 'Linux',
|
||||
secondaryDesc: resource.addr,
|
||||
},
|
||||
listViewProps: {
|
||||
resourceType: 'Linux',
|
||||
addr: resource.addr,
|
||||
},
|
||||
requiresRequest: resource.requiresRequest,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeUnifiedResourceViewItemUserGroup(
|
||||
resource: UnifiedResourceUserGroup,
|
||||
ui: UnifiedResourceUi
|
||||
@@ -255,6 +277,8 @@ export function mapResourceToViewItem({ resource, ui }: SharedUnifiedResource) {
|
||||
return makeUnifiedResourceViewItemApp(resource, ui);
|
||||
case 'windows_desktop':
|
||||
return makeUnifiedResourceViewItemDesktop(resource, ui);
|
||||
case 'linux_desktop':
|
||||
return makeUnifiedResourceViewItemLinuxDesktop(resource, ui);
|
||||
case 'user_group':
|
||||
return makeUnifiedResourceViewItemUserGroup(resource, ui);
|
||||
case 'git_server':
|
||||
|
||||
@@ -125,7 +125,7 @@ export interface UnifiedResourceKube {
|
||||
}
|
||||
|
||||
export type UnifiedResourceDesktop = {
|
||||
kind: 'windows_desktop';
|
||||
kind: 'windows_desktop' | 'linux_desktop';
|
||||
os: 'windows' | 'linux' | 'darwin';
|
||||
name: string;
|
||||
addr: string;
|
||||
|
||||
@@ -232,7 +232,8 @@ function getResourcesSubsections({
|
||||
searchableTags: ['resources', 'desktops', 'rdp', 'windows'],
|
||||
category: NavigationCategory.Resources,
|
||||
exact: false,
|
||||
customRouteMatchFn: () => isKindActive('windows_desktop'),
|
||||
customRouteMatchFn: () =>
|
||||
isKindActive('windows_desktop') || isKindActive('linux_desktop'),
|
||||
onClick: () => setPinnedUserPreference(false),
|
||||
subCategory: CustomNavigationSubcategory.FilteredViews,
|
||||
},
|
||||
|
||||
@@ -93,6 +93,7 @@ export type ResourceIdKind =
|
||||
| 'kube_cluster'
|
||||
| 'user_group'
|
||||
| 'windows_desktop'
|
||||
| 'linux_desktop'
|
||||
| 'saml_idp_service_provider'
|
||||
| 'aws_ic_account_assignment'
|
||||
| 'git_server';
|
||||
|
||||
@@ -34,6 +34,7 @@ test('correct formatting of desktops fetch response', async () => {
|
||||
addr: '10.0.0.10',
|
||||
labels: [{ name: 'env', value: 'test' }],
|
||||
logins: ['Administrator'],
|
||||
host_id: undefined,
|
||||
},
|
||||
],
|
||||
startKey: mockResponse.startKey,
|
||||
@@ -67,6 +68,7 @@ test('null labels field in desktops fetch response', async () => {
|
||||
const mockResponse = {
|
||||
items: [
|
||||
{
|
||||
kind: 'windows_desktop',
|
||||
addr: '10.0.0.10',
|
||||
labels: [{ name: 'env', value: 'test' }],
|
||||
name: 'DC1-teleport-demo',
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
import type { Desktop } from './types';
|
||||
|
||||
export function makeDesktop(json): Desktop {
|
||||
const { os, name, addr, host_id, requiresRequest } = json;
|
||||
const { os, name, addr, host_id, requiresRequest, kind } = json;
|
||||
|
||||
const labels = json.labels || [];
|
||||
const logins = json.logins || [];
|
||||
|
||||
return {
|
||||
kind: 'windows_desktop',
|
||||
kind,
|
||||
os,
|
||||
name,
|
||||
addr,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ResourceLabel } from 'teleport/services/agents';
|
||||
|
||||
// Desktop is a remote desktop.
|
||||
export type Desktop = {
|
||||
kind: 'windows_desktop';
|
||||
kind: 'windows_desktop' | 'linux_desktop';
|
||||
// OS is the os of this desktop.
|
||||
os: 'windows' | 'linux' | 'darwin';
|
||||
// Name is name (uuid) of the windows desktop.
|
||||
|
||||
@@ -39,6 +39,8 @@ export function makeUnifiedResource(json: any): UnifiedResource {
|
||||
return makeNode(json);
|
||||
case 'windows_desktop':
|
||||
return makeDesktop(json);
|
||||
case 'linux_desktop':
|
||||
return makeDesktop(json);
|
||||
case 'git_server':
|
||||
return makeGitServer(json);
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user