From 17586ed5bce656608a2a691ea2b403814049e8a8 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 17 Oct 2024 16:45:00 -0500 Subject: [PATCH 01/61] Validate redirect URL origin during app authentication (#47640) --- lib/web/app/redirect.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/web/app/redirect.go b/lib/web/app/redirect.go index 9aba5cca7bc8b..48f959478bee9 100644 --- a/lib/web/app/redirect.go +++ b/lib/web/app/redirect.go @@ -103,8 +103,9 @@ const appRedirectHTML = ` Teleport Redirection Service + + + ` +``` + + +Update your app service to serve the apps like this (update your public addr to what makes sense for your cluster) +``` +app_service: + enabled: "yes" + debug_app: true + apps: + - name: client + uri: http://localhost:8080 + public_addr: client.avatus.sh + required_apps: + - api + - name: api + uri: http://localhost:8080 + public_addr: api.avatus.sh + cors: + allowed_origins: + - https://client.avatus.sh +``` + +Launch your cluster and make sure you are logged out of your api by going to `https://api.avatus.sh/teleport-logout` + +- [ ] Launch the client app and you should see `{"hello":"world"}` response +- [ ] You should see no CORS issues in the console + ## Access Requests Not available for OSS From b82b037f6e4a94a7ce8d84bf7ec010c5baff2771 Mon Sep 17 00:00:00 2001 From: Steven Martin Date: Tue, 22 Oct 2024 15:03:11 -0400 Subject: [PATCH 59/61] docs: update policy prereqs (#47784) --- .../teleport-policy/integrations/aws-sync.mdx | 11 ++++++----- .../teleport-policy/integrations/entra-id.mdx | 7 ++++--- .../teleport-policy/integrations/gitlab.mdx | 13 +++++++------ .../teleport-policy/integrations/ssh-keys-scan.mdx | 13 +++++++------ 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/docs/pages/admin-guides/teleport-policy/integrations/aws-sync.mdx b/docs/pages/admin-guides/teleport-policy/integrations/aws-sync.mdx index cb35dc9c2c067..2aae7cd963fb5 100644 --- a/docs/pages/admin-guides/teleport-policy/integrations/aws-sync.mdx +++ b/docs/pages/admin-guides/teleport-policy/integrations/aws-sync.mdx @@ -60,12 +60,13 @@ graphical representation thereof. ## Prerequisites - A running Teleport Enterprise cluster v14.3.9/v15.2.0 or later. -- For self-hosted clusters, an updated `license.pem` with Teleport Policy enabled. -- For self-hosted clusters, a running Access Graph node v1.17.0 or later. -Check [Access Graph page](../teleport-policy.mdx) for details on +- Teleport Policy enabled for your account. +- For self-hosted clusters: + - Ensure that an up-to-date `license.pem` is used in the Auth Service configuration. + - A running Access Graph node v1.17.0 or later. +Check the [Teleport Policy page](../teleport-policy.mdx) for details on how to set up Access Graph. -- The node running the Access Graph service must be reachable -from Teleport Auth Service and Discovery Service. + - The node running the Access Graph service must be reachable from the Teleport Auth Service. ## Step 1/2. Configure Discovery Service (Self-hosted only) diff --git a/docs/pages/admin-guides/teleport-policy/integrations/entra-id.mdx b/docs/pages/admin-guides/teleport-policy/integrations/entra-id.mdx index 67d9736ed8ff2..da9b9e7feff9b 100644 --- a/docs/pages/admin-guides/teleport-policy/integrations/entra-id.mdx +++ b/docs/pages/admin-guides/teleport-policy/integrations/entra-id.mdx @@ -35,11 +35,12 @@ These resources are then visualized using the graph representation detailed in t - A running Teleport Enterprise cluster v15.4.2/v16.0.0 or later. - Teleport Identity and Teleport Policy enabled for your account. - - For self-hosted clusters, ensure that an up-to-date `license.pem` is used in the Auth Service configuration. -- For self-hosted clusters, a running Access Graph node v1.21.3 or later. +- For self-hosted clusters: + - Ensure that an up-to-date `license.pem` is used in the Auth Service configuration. + - A running Access Graph node v1.21.3 or later. Check the [Teleport Policy page](../teleport-policy.mdx) for details on how to set up Access Graph. -- The node running the Access Graph service must be reachable from the Teleport Auth Service. + - The node running the Access Graph service must be reachable from the Teleport Auth Service. - Your user must have privileged administrator permissions in the Azure account To verify that Access Graph is set up correctly for your cluster, sign in to the Teleport Web UI and navigate to the Management tab. diff --git a/docs/pages/admin-guides/teleport-policy/integrations/gitlab.mdx b/docs/pages/admin-guides/teleport-policy/integrations/gitlab.mdx index 83cc193507070..3a25ef7ad225f 100644 --- a/docs/pages/admin-guides/teleport-policy/integrations/gitlab.mdx +++ b/docs/pages/admin-guides/teleport-policy/integrations/gitlab.mdx @@ -46,13 +46,14 @@ graphical representation thereof. ## Prerequisites - A running Teleport Enterprise cluster v14.3.20/v15.3.1/v16.0.0 or later. -- For self-hosted clusters, an updated `license.pem` with Teleport Policy enabled. -- For self-hosted clusters, a running Access Graph node v1.21.4 or later. -Check [Access Graph page](../teleport-policy.mdx) for details on -how to set up Access Graph. -- For self-hosted clusters, the node running the Access Graph service must be reachable -from Teleport Auth Service. +- Teleport Policy enabled for your account. - A GitLab instance running GitLab v9.0 or later. +- For self-hosted clusters: + - Ensure that an up-to-date `license.pem` is used in the Auth Service configuration. + - A running Access Graph node v1.21.4 or later. +Check the [Teleport Policy page](../teleport-policy.mdx) for details on +how to set up Access Graph. + - The node running the Access Graph service must be reachable from the Teleport Auth Service. ## Step 1/3. Create GitLab token diff --git a/docs/pages/admin-guides/teleport-policy/integrations/ssh-keys-scan.mdx b/docs/pages/admin-guides/teleport-policy/integrations/ssh-keys-scan.mdx index 8aa1b8eac451a..8c50d3ad2da9d 100644 --- a/docs/pages/admin-guides/teleport-policy/integrations/ssh-keys-scan.mdx +++ b/docs/pages/admin-guides/teleport-policy/integrations/ssh-keys-scan.mdx @@ -70,15 +70,16 @@ It also never sends the private key path or any other sensitive information. ## Prerequisites - A running Teleport Enterprise cluster v15.4.16/v16.2.0 or later. -- For self-hosted clusters, an updated `license.pem` with Teleport Policy enabled. -- For self-hosted clusters, a running Access Graph node v1.22.0 or later. -Check [Access Graph page](../teleport-policy.mdx) for details on -how to set up Access Graph. -- For self-hosted clusters, the node running the Access Graph service must be reachable -from Teleport Auth Service. +- Teleport Policy enabled for your account. - A Linux/macOS server running the Teleport SSH Service. - Devices enrolled in the [Teleport Device Trust feature](../../access-controls/device-trust.mdx). - For Jamf Pro integration, devices must be enrolled in Jamf Pro and have the signed `tsh` binary installed. +- For self-hosted clusters: + - Ensure that an up-to-date `license.pem` is used in the Auth Service configuration. + - A running Access Graph node v1.22.0 or later. +Check the [Teleport Policy page](../teleport-policy.mdx) for details on +how to set up Access Graph. + - The node running the Access Graph service must be reachable from the Teleport Auth Service. ## Step 1/3. Enable SSH Key Scanning From 0f487f6bbcf29caa61b9576de9d9ef6769b4d82d Mon Sep 17 00:00:00 2001 From: Przemko Robakowski Date: Tue, 22 Oct 2024 22:12:34 +0200 Subject: [PATCH 60/61] Implement API and backend for DynamicWindowsDesktop (#46987) * Add DynamicWindowsDesktop to proto * Add resource matchers to Windows desktop service config * Implement API and backend for DynamicWindowsDesktop * Fix imports * move rpc to separate server * rework api and grpc more towards 153-style * e * remove dynamic windows from paginated resource * add tests * add tests * Update api/proto/teleport/legacy/types/types.proto Co-authored-by: rosstimothy <39066650+rosstimothy@users.noreply.github.com> * lint * gci * cleanup * cleanup * use generic service * cleanup * cleanup * cleanup * cleanup * cleanup * gci * add admin action checks * move service * add service test * gci * review comments * review comments * review comments * review comments * review comments --------- Co-authored-by: rosstimothy <39066650+rosstimothy@users.noreply.github.com> --- api/client/client.go | 6 + api/client/dynamicwindows/dynamicwindows.go | 93 +++++++ api/types/constants.go | 3 + api/types/derived.gen.go | 188 +++++++------- api/types/desktop.go | 203 ++++++++++++++- api/types/role.go | 2 + lib/auth/auth.go | 8 + .../dynamicwindowsv1/service.go | 215 ++++++++++++++++ .../dynamicwindowsv1/service_test.go | 220 +++++++++++++++++ lib/auth/grpcserver.go | 13 + lib/auth/init.go | 5 +- lib/authz/permissions.go | 1 + lib/services/dynamic_desktop.go | 90 +++++++ lib/services/local/dynamic_desktops.go | 119 +++++++++ lib/services/local/dynamic_desktops_test.go | 232 ++++++++++++++++++ lib/services/resource.go | 2 + lib/services/role.go | 1 + 17 files changed, 1303 insertions(+), 98 deletions(-) create mode 100644 api/client/dynamicwindows/dynamicwindows.go create mode 100644 lib/auth/dynamicwindows/dynamicwindowsv1/service.go create mode 100644 lib/auth/dynamicwindows/dynamicwindowsv1/service_test.go create mode 100644 lib/services/dynamic_desktop.go create mode 100644 lib/services/local/dynamic_desktops.go create mode 100644 lib/services/local/dynamic_desktops_test.go diff --git a/api/client/client.go b/api/client/client.go index 8980e42ac16e8..fc08ad53467c7 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -53,6 +53,7 @@ import ( "github.com/gravitational/teleport/api/client/accessmonitoringrules" crownjewelapi "github.com/gravitational/teleport/api/client/crownjewel" "github.com/gravitational/teleport/api/client/discoveryconfig" + "github.com/gravitational/teleport/api/client/dynamicwindows" "github.com/gravitational/teleport/api/client/externalauditstorage" kubewaitingcontainerclient "github.com/gravitational/teleport/api/client/kubewaitingcontainer" "github.com/gravitational/teleport/api/client/okta" @@ -74,6 +75,7 @@ import ( dbobjectimportrulev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1" devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1" discoveryconfigv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/discoveryconfig/v1" + dynamicwindowsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dynamicwindows/v1" externalauditstoragev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/externalauditstorage/v1" integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" kubeproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/kube/v1" @@ -2657,6 +2659,10 @@ func (c *Client) SearchSessionEvents(ctx context.Context, fromUTC time.Time, toU return decodedEvents, response.LastKey, nil } +func (c *Client) DynamicDesktopClient() *dynamicwindows.Client { + return dynamicwindows.NewClient(dynamicwindowsv1.NewDynamicWindowsServiceClient(c.conn)) +} + // ClusterConfigClient returns an unadorned Cluster Configuration client, using the underlying // Auth gRPC connection. func (c *Client) ClusterConfigClient() clusterconfigpb.ClusterConfigServiceClient { diff --git a/api/client/dynamicwindows/dynamicwindows.go b/api/client/dynamicwindows/dynamicwindows.go new file mode 100644 index 0000000000000..6c158a39e4243 --- /dev/null +++ b/api/client/dynamicwindows/dynamicwindows.go @@ -0,0 +1,93 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dynamicwindows + +import ( + "context" + + "github.com/gravitational/trace" + + dynamicwindows "github.com/gravitational/teleport/api/gen/proto/go/teleport/dynamicwindows/v1" + "github.com/gravitational/teleport/api/types" +) + +// Client is a DynamicWindowsDesktop client. +type Client struct { + grpcClient dynamicwindows.DynamicWindowsServiceClient +} + +// NewClient creates a new StaticHostUser client. +func NewClient(grpcClient dynamicwindows.DynamicWindowsServiceClient) *Client { + return &Client{ + grpcClient: grpcClient, + } +} + +func (c *Client) GetDynamicWindowsDesktop(ctx context.Context, name string) (types.DynamicWindowsDesktop, error) { + desktop, err := c.grpcClient.GetDynamicWindowsDesktop(ctx, &dynamicwindows.GetDynamicWindowsDesktopRequest{ + Name: name, + }) + return desktop, trace.Wrap(err) +} + +func (c *Client) ListDynamicWindowsDesktop(ctx context.Context, pageSize int, pageToken string) ([]types.DynamicWindowsDesktop, string, error) { + resp, err := c.grpcClient.ListDynamicWindowsDesktops(ctx, &dynamicwindows.ListDynamicWindowsDesktopsRequest{ + PageSize: int32(pageSize), + PageToken: pageToken, + }) + if err != nil { + return nil, "", trace.Wrap(err) + } + desktops := make([]types.DynamicWindowsDesktop, 0, len(resp.GetDesktops())) + for _, desktop := range resp.GetDesktops() { + desktops = append(desktops, desktop) + } + return desktops, resp.GetNextPageToken(), nil +} + +func (c *Client) CreateDynamicWindowsDesktop(ctx context.Context, desktop types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) { + switch desktop := desktop.(type) { + case *types.DynamicWindowsDesktopV1: + desktop, err := c.grpcClient.CreateDynamicWindowsDesktop(ctx, &dynamicwindows.CreateDynamicWindowsDesktopRequest{ + Desktop: desktop, + }) + return desktop, trace.Wrap(err) + default: + return nil, trace.BadParameter("unknown desktop type: %T", desktop) + } +} + +func (c *Client) UpdateDynamicWindowsDesktop(ctx context.Context, desktop types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) { + switch desktop := desktop.(type) { + case *types.DynamicWindowsDesktopV1: + desktop, err := c.grpcClient.UpdateDynamicWindowsDesktop(ctx, &dynamicwindows.UpdateDynamicWindowsDesktopRequest{ + Desktop: desktop, + }) + return desktop, trace.Wrap(err) + default: + return nil, trace.BadParameter("unknown desktop type: %T", desktop) + } +} + +func (c *Client) DeleteDynamicWindowsDesktop(ctx context.Context, name string) error { + _, err := c.grpcClient.DeleteDynamicWindowsDesktop(ctx, &dynamicwindows.DeleteDynamicWindowsDesktopRequest{ + Name: name, + }) + return trace.Wrap(err) +} diff --git a/api/types/constants.go b/api/types/constants.go index 36e2571891067..87c0335586bf6 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -423,6 +423,9 @@ const ( // KindWindowsDesktop is a Windows desktop host. KindWindowsDesktop = "windows_desktop" + // KindDynamicWindowsDesktop is a dynamic Windows desktop host. + KindDynamicWindowsDesktop = "dynamic_windows_desktop" + // KindRecoveryCodes is a resource that holds users recovery codes. KindRecoveryCodes = "recovery_codes" diff --git a/api/types/derived.gen.go b/api/types/derived.gen.go index 6f2cd5467ee9d..0f96c2afaa430 100644 --- a/api/types/derived.gen.go +++ b/api/types/derived.gen.go @@ -77,12 +77,20 @@ func deriveTeleportEqualDatabaseV3(this, that *DatabaseV3) bool { deriveTeleportEqual_11(&this.Spec, &that.Spec) } +// deriveTeleportEqualDynamicWindowsDesktopV1 returns whether this and that are equal. +func deriveTeleportEqualDynamicWindowsDesktopV1(this, that *DynamicWindowsDesktopV1) bool { + return (this == nil && that == nil) || + this != nil && that != nil && + deriveTeleportEqualResourceHeader(&this.ResourceHeader, &that.ResourceHeader) && + deriveTeleportEqual_12(&this.Spec, &that.Spec) +} + // deriveTeleportEqualWindowsDesktopV3 returns whether this and that are equal. func deriveTeleportEqualWindowsDesktopV3(this, that *WindowsDesktopV3) bool { return (this == nil && that == nil) || this != nil && that != nil && deriveTeleportEqualResourceHeader(&this.ResourceHeader, &that.ResourceHeader) && - deriveTeleportEqual_12(&this.Spec, &that.Spec) + deriveTeleportEqual_13(&this.Spec, &that.Spec) } // deriveTeleportEqualKubeAzure returns whether this and that are equal. @@ -121,7 +129,7 @@ func deriveTeleportEqualKubernetesClusterV3(this, that *KubernetesClusterV3) boo this.SubKind == that.SubKind && this.Version == that.Version && deriveTeleportEqualMetadata(&this.Metadata, &that.Metadata) && - deriveTeleportEqual_13(&this.Spec, &that.Spec) + deriveTeleportEqual_14(&this.Spec, &that.Spec) } // deriveTeleportEqualKubernetesServerV3 returns whether this and that are equal. @@ -132,7 +140,7 @@ func deriveTeleportEqualKubernetesServerV3(this, that *KubernetesServerV3) bool this.SubKind == that.SubKind && this.Version == that.Version && deriveTeleportEqualMetadata(&this.Metadata, &that.Metadata) && - deriveTeleportEqual_14(&this.Spec, &that.Spec) + deriveTeleportEqual_15(&this.Spec, &that.Spec) } // deriveTeleportEqualOktaAssignmentV1 returns whether this and that are equal. @@ -140,7 +148,7 @@ func deriveTeleportEqualOktaAssignmentV1(this, that *OktaAssignmentV1) bool { return (this == nil && that == nil) || this != nil && that != nil && deriveTeleportEqualResourceHeader(&this.ResourceHeader, &that.ResourceHeader) && - deriveTeleportEqual_15(&this.Spec, &that.Spec) + deriveTeleportEqual_16(&this.Spec, &that.Spec) } // deriveTeleportEqualResourceHeader returns whether this and that are equal. @@ -169,7 +177,7 @@ func deriveTeleportEqualUserGroupV1(this, that *UserGroupV1) bool { return (this == nil && that == nil) || this != nil && that != nil && deriveTeleportEqualResourceHeader(&this.ResourceHeader, &that.ResourceHeader) && - deriveTeleportEqual_16(&this.Spec, &that.Spec) + deriveTeleportEqual_17(&this.Spec, &that.Spec) } // deriveTeleportEqual returns whether this and that are equal. @@ -178,15 +186,15 @@ func deriveTeleportEqual(this, that *AppSpecV3) bool { this != nil && that != nil && this.URI == that.URI && this.PublicAddr == that.PublicAddr && - deriveTeleportEqual_17(this.DynamicLabels, that.DynamicLabels) && + deriveTeleportEqual_18(this.DynamicLabels, that.DynamicLabels) && this.InsecureSkipVerify == that.InsecureSkipVerify && - deriveTeleportEqual_18(this.Rewrite, that.Rewrite) && - deriveTeleportEqual_19(this.AWS, that.AWS) && + deriveTeleportEqual_19(this.Rewrite, that.Rewrite) && + deriveTeleportEqual_20(this.AWS, that.AWS) && this.Cloud == that.Cloud && - deriveTeleportEqual_20(this.UserGroups, that.UserGroups) && + deriveTeleportEqual_21(this.UserGroups, that.UserGroups) && this.Integration == that.Integration && - deriveTeleportEqual_20(this.RequiredAppNames, that.RequiredAppNames) && - deriveTeleportEqual_21(this.CORS, that.CORS) + deriveTeleportEqual_21(this.RequiredAppNames, that.RequiredAppNames) && + deriveTeleportEqual_22(this.CORS, that.CORS) } // deriveTeleportEqual_ returns whether this and that are equal. @@ -204,9 +212,9 @@ func deriveTeleportEqual_1(this, that *RDS) bool { this.ClusterID == that.ClusterID && this.ResourceID == that.ResourceID && this.IAMAuth == that.IAMAuth && - deriveTeleportEqual_20(this.Subnets, that.Subnets) && + deriveTeleportEqual_21(this.Subnets, that.Subnets) && this.VPCID == that.VPCID && - deriveTeleportEqual_20(this.SecurityGroups, that.SecurityGroups) + deriveTeleportEqual_21(this.SecurityGroups, that.SecurityGroups) } // deriveTeleportEqual_2 returns whether this and that are equal. @@ -214,7 +222,7 @@ func deriveTeleportEqual_2(this, that *ElastiCache) bool { return (this == nil && that == nil) || this != nil && that != nil && this.ReplicationGroupID == that.ReplicationGroupID && - deriveTeleportEqual_20(this.UserGroupIDs, that.UserGroupIDs) && + deriveTeleportEqual_21(this.UserGroupIDs, that.UserGroupIDs) && this.TransitEncryptionEnabled == that.TransitEncryptionEnabled && this.EndpointType == that.EndpointType } @@ -307,73 +315,83 @@ func deriveTeleportEqual_11(this, that *DatabaseSpecV3) bool { this.Protocol == that.Protocol && this.URI == that.URI && this.CACert == that.CACert && - deriveTeleportEqual_17(this.DynamicLabels, that.DynamicLabels) && + deriveTeleportEqual_18(this.DynamicLabels, that.DynamicLabels) && deriveTeleportEqualAWS(&this.AWS, &that.AWS) && deriveTeleportEqualGCPCloudSQL(&this.GCP, &that.GCP) && deriveTeleportEqualAzure(&this.Azure, &that.Azure) && - deriveTeleportEqual_22(&this.TLS, &that.TLS) && - deriveTeleportEqual_23(&this.AD, &that.AD) && - deriveTeleportEqual_24(&this.MySQL, &that.MySQL) && - deriveTeleportEqual_25(this.AdminUser, that.AdminUser) && - deriveTeleportEqual_26(&this.MongoAtlas, &that.MongoAtlas) && - deriveTeleportEqual_27(&this.Oracle, &that.Oracle) + deriveTeleportEqual_23(&this.TLS, &that.TLS) && + deriveTeleportEqual_24(&this.AD, &that.AD) && + deriveTeleportEqual_25(&this.MySQL, &that.MySQL) && + deriveTeleportEqual_26(this.AdminUser, that.AdminUser) && + deriveTeleportEqual_27(&this.MongoAtlas, &that.MongoAtlas) && + deriveTeleportEqual_28(&this.Oracle, &that.Oracle) } // deriveTeleportEqual_12 returns whether this and that are equal. -func deriveTeleportEqual_12(this, that *WindowsDesktopSpecV3) bool { +func deriveTeleportEqual_12(this, that *DynamicWindowsDesktopSpecV1) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Addr == that.Addr && this.Domain == that.Domain && - this.HostID == that.HostID && this.NonAD == that.NonAD && - deriveTeleportEqual_28(this.ScreenSize, that.ScreenSize) + deriveTeleportEqual_29(this.ScreenSize, that.ScreenSize) } // deriveTeleportEqual_13 returns whether this and that are equal. -func deriveTeleportEqual_13(this, that *KubernetesClusterSpecV3) bool { +func deriveTeleportEqual_13(this, that *WindowsDesktopSpecV3) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_17(this.DynamicLabels, that.DynamicLabels) && + this.Addr == that.Addr && + this.Domain == that.Domain && + this.HostID == that.HostID && + this.NonAD == that.NonAD && + deriveTeleportEqual_29(this.ScreenSize, that.ScreenSize) +} + +// deriveTeleportEqual_14 returns whether this and that are equal. +func deriveTeleportEqual_14(this, that *KubernetesClusterSpecV3) bool { + return (this == nil && that == nil) || + this != nil && that != nil && + deriveTeleportEqual_18(this.DynamicLabels, that.DynamicLabels) && bytes.Equal(this.Kubeconfig, that.Kubeconfig) && deriveTeleportEqualKubeAzure(&this.Azure, &that.Azure) && deriveTeleportEqualKubeAWS(&this.AWS, &that.AWS) && deriveTeleportEqualKubeGCP(&this.GCP, &that.GCP) } -// deriveTeleportEqual_14 returns whether this and that are equal. -func deriveTeleportEqual_14(this, that *KubernetesServerSpecV3) bool { +// deriveTeleportEqual_15 returns whether this and that are equal. +func deriveTeleportEqual_15(this, that *KubernetesServerSpecV3) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Version == that.Version && this.Hostname == that.Hostname && this.HostID == that.HostID && - deriveTeleportEqual_29(&this.Rotation, &that.Rotation) && + deriveTeleportEqual_30(&this.Rotation, &that.Rotation) && deriveTeleportEqualKubernetesClusterV3(this.Cluster, that.Cluster) && - deriveTeleportEqual_20(this.ProxyIDs, that.ProxyIDs) + deriveTeleportEqual_21(this.ProxyIDs, that.ProxyIDs) } -// deriveTeleportEqual_15 returns whether this and that are equal. -func deriveTeleportEqual_15(this, that *OktaAssignmentSpecV1) bool { +// deriveTeleportEqual_16 returns whether this and that are equal. +func deriveTeleportEqual_16(this, that *OktaAssignmentSpecV1) bool { return (this == nil && that == nil) || this != nil && that != nil && this.User == that.User && - deriveTeleportEqual_30(this.Targets, that.Targets) && + deriveTeleportEqual_31(this.Targets, that.Targets) && this.CleanupTime.Equal(that.CleanupTime) && this.Status == that.Status && this.LastTransition.Equal(that.LastTransition) && this.Finalized == that.Finalized } -// deriveTeleportEqual_16 returns whether this and that are equal. -func deriveTeleportEqual_16(this, that *UserGroupSpecV1) bool { +// deriveTeleportEqual_17 returns whether this and that are equal. +func deriveTeleportEqual_17(this, that *UserGroupSpecV1) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_20(this.Applications, that.Applications) + deriveTeleportEqual_21(this.Applications, that.Applications) } -// deriveTeleportEqual_17 returns whether this and that are equal. -func deriveTeleportEqual_17(this, that map[string]CommandLabelV2) bool { +// deriveTeleportEqual_18 returns whether this and that are equal. +func deriveTeleportEqual_18(this, that map[string]CommandLabelV2) bool { if this == nil || that == nil { return this == nil && that == nil } @@ -385,31 +403,31 @@ func deriveTeleportEqual_17(this, that map[string]CommandLabelV2) bool { if !ok { return false } - if !(deriveTeleportEqual_31(&v, &thatv)) { + if !(deriveTeleportEqual_32(&v, &thatv)) { return false } } return true } -// deriveTeleportEqual_18 returns whether this and that are equal. -func deriveTeleportEqual_18(this, that *Rewrite) bool { +// deriveTeleportEqual_19 returns whether this and that are equal. +func deriveTeleportEqual_19(this, that *Rewrite) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_20(this.Redirect, that.Redirect) && - deriveTeleportEqual_32(this.Headers, that.Headers) && + deriveTeleportEqual_21(this.Redirect, that.Redirect) && + deriveTeleportEqual_33(this.Headers, that.Headers) && this.JWTClaims == that.JWTClaims } -// deriveTeleportEqual_19 returns whether this and that are equal. -func deriveTeleportEqual_19(this, that *AppAWS) bool { +// deriveTeleportEqual_20 returns whether this and that are equal. +func deriveTeleportEqual_20(this, that *AppAWS) bool { return (this == nil && that == nil) || this != nil && that != nil && this.ExternalID == that.ExternalID } -// deriveTeleportEqual_20 returns whether this and that are equal. -func deriveTeleportEqual_20(this, that []string) bool { +// deriveTeleportEqual_21 returns whether this and that are equal. +func deriveTeleportEqual_21(this, that []string) bool { if this == nil || that == nil { return this == nil && that == nil } @@ -424,20 +442,20 @@ func deriveTeleportEqual_20(this, that []string) bool { return true } -// deriveTeleportEqual_21 returns whether this and that are equal. -func deriveTeleportEqual_21(this, that *CORSPolicy) bool { +// deriveTeleportEqual_22 returns whether this and that are equal. +func deriveTeleportEqual_22(this, that *CORSPolicy) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_20(this.AllowedOrigins, that.AllowedOrigins) && - deriveTeleportEqual_20(this.AllowedMethods, that.AllowedMethods) && - deriveTeleportEqual_20(this.AllowedHeaders, that.AllowedHeaders) && + deriveTeleportEqual_21(this.AllowedOrigins, that.AllowedOrigins) && + deriveTeleportEqual_21(this.AllowedMethods, that.AllowedMethods) && + deriveTeleportEqual_21(this.AllowedHeaders, that.AllowedHeaders) && this.AllowCredentials == that.AllowCredentials && this.MaxAge == that.MaxAge && - deriveTeleportEqual_20(this.ExposedHeaders, that.ExposedHeaders) + deriveTeleportEqual_21(this.ExposedHeaders, that.ExposedHeaders) } -// deriveTeleportEqual_22 returns whether this and that are equal. -func deriveTeleportEqual_22(this, that *DatabaseTLS) bool { +// deriveTeleportEqual_23 returns whether this and that are equal. +func deriveTeleportEqual_23(this, that *DatabaseTLS) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Mode == that.Mode && @@ -446,8 +464,8 @@ func deriveTeleportEqual_22(this, that *DatabaseTLS) bool { this.TrustSystemCertPool == that.TrustSystemCertPool } -// deriveTeleportEqual_23 returns whether this and that are equal. -func deriveTeleportEqual_23(this, that *AD) bool { +// deriveTeleportEqual_24 returns whether this and that are equal. +func deriveTeleportEqual_24(this, that *AD) bool { return (this == nil && that == nil) || this != nil && that != nil && this.KeytabFile == that.KeytabFile && @@ -458,45 +476,45 @@ func deriveTeleportEqual_23(this, that *AD) bool { this.KDCHostName == that.KDCHostName } -// deriveTeleportEqual_24 returns whether this and that are equal. -func deriveTeleportEqual_24(this, that *MySQLOptions) bool { +// deriveTeleportEqual_25 returns whether this and that are equal. +func deriveTeleportEqual_25(this, that *MySQLOptions) bool { return (this == nil && that == nil) || this != nil && that != nil && this.ServerVersion == that.ServerVersion } -// deriveTeleportEqual_25 returns whether this and that are equal. -func deriveTeleportEqual_25(this, that *DatabaseAdminUser) bool { +// deriveTeleportEqual_26 returns whether this and that are equal. +func deriveTeleportEqual_26(this, that *DatabaseAdminUser) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Name == that.Name && this.DefaultDatabase == that.DefaultDatabase } -// deriveTeleportEqual_26 returns whether this and that are equal. -func deriveTeleportEqual_26(this, that *MongoAtlas) bool { +// deriveTeleportEqual_27 returns whether this and that are equal. +func deriveTeleportEqual_27(this, that *MongoAtlas) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Name == that.Name } -// deriveTeleportEqual_27 returns whether this and that are equal. -func deriveTeleportEqual_27(this, that *OracleOptions) bool { +// deriveTeleportEqual_28 returns whether this and that are equal. +func deriveTeleportEqual_28(this, that *OracleOptions) bool { return (this == nil && that == nil) || this != nil && that != nil && this.AuditUser == that.AuditUser } -// deriveTeleportEqual_28 returns whether this and that are equal. -func deriveTeleportEqual_28(this, that *Resolution) bool { +// deriveTeleportEqual_29 returns whether this and that are equal. +func deriveTeleportEqual_29(this, that *Resolution) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Width == that.Width && this.Height == that.Height } -// deriveTeleportEqual_29 returns whether this and that are equal. -func deriveTeleportEqual_29(this, that *Rotation) bool { +// deriveTeleportEqual_30 returns whether this and that are equal. +func deriveTeleportEqual_30(this, that *Rotation) bool { return (this == nil && that == nil) || this != nil && that != nil && this.State == that.State && @@ -506,11 +524,11 @@ func deriveTeleportEqual_29(this, that *Rotation) bool { this.Started.Equal(that.Started) && this.GracePeriod == that.GracePeriod && this.LastRotated.Equal(that.LastRotated) && - deriveTeleportEqual_33(&this.Schedule, &that.Schedule) + deriveTeleportEqual_34(&this.Schedule, &that.Schedule) } -// deriveTeleportEqual_30 returns whether this and that are equal. -func deriveTeleportEqual_30(this, that []*OktaAssignmentTargetV1) bool { +// deriveTeleportEqual_31 returns whether this and that are equal. +func deriveTeleportEqual_31(this, that []*OktaAssignmentTargetV1) bool { if this == nil || that == nil { return this == nil && that == nil } @@ -518,24 +536,24 @@ func deriveTeleportEqual_30(this, that []*OktaAssignmentTargetV1) bool { return false } for i := 0; i < len(this); i++ { - if !(deriveTeleportEqual_34(this[i], that[i])) { + if !(deriveTeleportEqual_35(this[i], that[i])) { return false } } return true } -// deriveTeleportEqual_31 returns whether this and that are equal. -func deriveTeleportEqual_31(this, that *CommandLabelV2) bool { +// deriveTeleportEqual_32 returns whether this and that are equal. +func deriveTeleportEqual_32(this, that *CommandLabelV2) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Period == that.Period && - deriveTeleportEqual_20(this.Command, that.Command) && + deriveTeleportEqual_21(this.Command, that.Command) && this.Result == that.Result } -// deriveTeleportEqual_32 returns whether this and that are equal. -func deriveTeleportEqual_32(this, that []*Header) bool { +// deriveTeleportEqual_33 returns whether this and that are equal. +func deriveTeleportEqual_33(this, that []*Header) bool { if this == nil || that == nil { return this == nil && that == nil } @@ -543,15 +561,15 @@ func deriveTeleportEqual_32(this, that []*Header) bool { return false } for i := 0; i < len(this); i++ { - if !(deriveTeleportEqual_35(this[i], that[i])) { + if !(deriveTeleportEqual_36(this[i], that[i])) { return false } } return true } -// deriveTeleportEqual_33 returns whether this and that are equal. -func deriveTeleportEqual_33(this, that *RotationSchedule) bool { +// deriveTeleportEqual_34 returns whether this and that are equal. +func deriveTeleportEqual_34(this, that *RotationSchedule) bool { return (this == nil && that == nil) || this != nil && that != nil && this.UpdateClients.Equal(that.UpdateClients) && @@ -559,16 +577,16 @@ func deriveTeleportEqual_33(this, that *RotationSchedule) bool { this.Standby.Equal(that.Standby) } -// deriveTeleportEqual_34 returns whether this and that are equal. -func deriveTeleportEqual_34(this, that *OktaAssignmentTargetV1) bool { +// deriveTeleportEqual_35 returns whether this and that are equal. +func deriveTeleportEqual_35(this, that *OktaAssignmentTargetV1) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Type == that.Type && this.Id == that.Id } -// deriveTeleportEqual_35 returns whether this and that are equal. -func deriveTeleportEqual_35(this, that *Header) bool { +// deriveTeleportEqual_36 returns whether this and that are equal. +func deriveTeleportEqual_36(this, that *Header) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Name == that.Name && diff --git a/api/types/desktop.go b/api/types/desktop.go index 23a8140a4deda..a6455484e2daa 100644 --- a/api/types/desktop.go +++ b/api/types/desktop.go @@ -127,6 +127,174 @@ func (s *WindowsDesktopServiceV3) MatchSearch(values []string) bool { return MatchSearch(fieldVals, values, nil) } +// DynamicWindowsDesktop represents a Windows desktop host that is automatically discovered by Windows Desktop Service. +type DynamicWindowsDesktop interface { + // ResourceWithLabels provides common resource methods. + ResourceWithLabels + // GetAddr returns the network address of this host. + GetAddr() string + // GetDomain returns the ActiveDirectory domain of this host. + GetDomain() string + // NonAD checks whether this is a standalone host that + // is not joined to an Active Directory domain. + NonAD() bool + // GetScreenSize returns the desired size of the screen to use for sessions + // to this host. Returns (0, 0) if no screen size is set, which means to + // use the size passed by the client over TDP. + GetScreenSize() (width, height uint32) + // Copy returns a copy of this dynamic Windows desktop + Copy() *DynamicWindowsDesktopV1 +} + +var _ DynamicWindowsDesktop = &DynamicWindowsDesktopV1{} + +// NewDynamicWindowsDesktopV1 creates a new DynamicWindowsDesktopV1 resource. +func NewDynamicWindowsDesktopV1(name string, labels map[string]string, spec DynamicWindowsDesktopSpecV1) (*DynamicWindowsDesktopV1, error) { + d := &DynamicWindowsDesktopV1{ + ResourceHeader: ResourceHeader{ + Metadata: Metadata{ + Name: name, + Labels: labels, + }, + }, + Spec: spec, + } + if err := d.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + return d, nil +} + +func (d *DynamicWindowsDesktopV1) setStaticFields() { + d.Kind = KindDynamicWindowsDesktop + d.Version = V1 +} + +// CheckAndSetDefaults checks and sets default values for any missing fields. +func (d *DynamicWindowsDesktopV1) CheckAndSetDefaults() error { + if d.Spec.Addr == "" { + return trace.BadParameter("DynamicWindowsDesktopV1.Spec missing Addr field") + } + + if err := checkNameAndScreenSize(d.GetName(), d.Spec.ScreenSize); err != nil { + return trace.Wrap(err) + } + + d.setStaticFields() + if err := d.ResourceHeader.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + + return nil +} + +func (d *DynamicWindowsDesktopV1) GetScreenSize() (width, height uint32) { + if d.Spec.ScreenSize == nil { + return 0, 0 + } + return d.Spec.ScreenSize.Width, d.Spec.ScreenSize.Height +} + +// NonAD checks whether host is part of Active Directory +func (d *DynamicWindowsDesktopV1) NonAD() bool { + return d.Spec.NonAD +} + +// GetAddr returns the network address of this host. +func (d *DynamicWindowsDesktopV1) GetAddr() string { + return d.Spec.Addr +} + +// GetDomain returns the Active Directory domain of this host. +func (d *DynamicWindowsDesktopV1) GetDomain() string { + return d.Spec.Domain +} + +// MatchSearch goes through select field values and tries to +// match against the list of search values. +func (d *DynamicWindowsDesktopV1) MatchSearch(values []string) bool { + fieldVals := append(utils.MapToStrings(d.GetAllLabels()), d.GetName(), d.GetAddr()) + return MatchSearch(fieldVals, values, nil) +} + +// Copy returns a deep copy of this dynamic Windows desktop object. +func (d *DynamicWindowsDesktopV1) Copy() *DynamicWindowsDesktopV1 { + return utils.CloneProtoMsg(d) +} + +// IsEqual determines if two dynamic Windows desktop resources are equivalent to one another. +func (d *DynamicWindowsDesktopV1) IsEqual(i DynamicWindowsDesktop) bool { + if other, ok := i.(*DynamicWindowsDesktopV1); ok { + return deriveTeleportEqualDynamicWindowsDesktopV1(d, other) + } + return false +} + +// DynamicWindowsDesktops represents a list of Windows desktops. +type DynamicWindowsDesktops []DynamicWindowsDesktop + +// Len returns the slice length. +func (s DynamicWindowsDesktops) Len() int { return len(s) } + +// Less compares desktops by name and host ID. +func (s DynamicWindowsDesktops) Less(i, j int) bool { + return s[i].GetName() < s[j].GetName() +} + +// Swap swaps two windows desktops. +func (s DynamicWindowsDesktops) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// SortByCustom custom sorts by given sort criteria. +func (s DynamicWindowsDesktops) SortByCustom(sortBy SortBy) error { + if sortBy.Field == "" { + return nil + } + + isDesc := sortBy.IsDesc + switch sortBy.Field { + case ResourceMetadataName: + sort.SliceStable(s, func(i, j int) bool { + return stringCompare(s[i].GetName(), s[j].GetName(), isDesc) + }) + case ResourceSpecAddr: + sort.SliceStable(s, func(i, j int) bool { + return stringCompare(s[i].GetAddr(), s[j].GetAddr(), isDesc) + }) + default: + return trace.NotImplemented("sorting by field %q for resource %q is not supported", sortBy.Field, KindDynamicWindowsDesktop) + } + + return nil +} + +// AsResources returns dynamic windows desktops as type resources with labels. +func (s DynamicWindowsDesktops) AsResources() []ResourceWithLabels { + resources := make([]ResourceWithLabels, 0, len(s)) + for _, server := range s { + resources = append(resources, ResourceWithLabels(server)) + } + return resources +} + +// GetFieldVals returns list of select field values. +func (s DynamicWindowsDesktops) GetFieldVals(field string) ([]string, error) { + vals := make([]string, 0, len(s)) + switch field { + case ResourceMetadataName: + for _, server := range s { + vals = append(vals, server.GetName()) + } + case ResourceSpecAddr: + for _, server := range s { + vals = append(vals, server.GetAddr()) + } + default: + return nil, trace.NotImplemented("getting field %q for resource %q is not supported", field, KindDynamicWindowsDesktop) + } + + return vals, nil +} + // WindowsDesktop represents a Windows desktop host. type WindowsDesktop interface { // ResourceWithLabels provides common resource methods. @@ -180,11 +348,8 @@ func (d *WindowsDesktopV3) CheckAndSetDefaults() error { return trace.BadParameter("WindowsDesktopV3.Spec missing Addr field") } - // We use SNI to identify the desktop to route a connection to, - // and '.' will add an extra subdomain, preventing Teleport from - // correctly establishing TLS connections. - if name := d.GetName(); strings.Contains(name, ".") { - return trace.BadParameter("invalid name %q: desktop names cannot contain periods", name) + if err := checkNameAndScreenSize(d.GetName(), d.Spec.ScreenSize); err != nil { + return trace.Wrap(err) } d.setStaticFields() @@ -192,13 +357,6 @@ func (d *WindowsDesktopV3) CheckAndSetDefaults() error { return trace.Wrap(err) } - if d.Spec.ScreenSize != nil { - if d.Spec.ScreenSize.Width > MaxRDPScreenWidth || d.Spec.ScreenSize.Height > MaxRDPScreenHeight { - return trace.BadParameter("invalid screen size %dx%d (maximum %dx%d)", - d.Spec.ScreenSize.Width, d.Spec.ScreenSize.Height, MaxRDPScreenWidth, MaxRDPScreenHeight) - } - } - return nil } @@ -351,6 +509,12 @@ type ListWindowsDesktopsRequest struct { SearchKeywords []string } +// ListDynamicWindowsDesktopsResponse is a response type to ListDynamicWindowsDesktops. +type ListDynamicWindowsDesktopsResponse struct { + Desktops []DynamicWindowsDesktop + NextKey string +} + // ListWindowsDesktopServicesResponse is a response type to ListWindowsDesktopServices. type ListWindowsDesktopServicesResponse struct { DesktopServices []WindowsDesktopService @@ -364,3 +528,18 @@ type ListWindowsDesktopServicesRequest struct { Labels map[string]string SearchKeywords []string } + +func checkNameAndScreenSize(name string, screenSize *Resolution) error { + // We use SNI to identify the desktop to route a connection to, + // and '.' will add an extra subdomain, preventing Teleport from + // correctly establishing TLS connections. + if strings.Contains(name, ".") { + return trace.BadParameter("invalid name %q: desktop names cannot contain periods", name) + } + + if screenSize != nil && (screenSize.Width > MaxRDPScreenWidth || screenSize.Height > MaxRDPScreenHeight) { + return trace.BadParameter("screen size %dx%d too big (maximum %dx%d)", + screenSize.Width, screenSize.Height, MaxRDPScreenWidth, MaxRDPScreenHeight) + } + return nil +} diff --git a/api/types/role.go b/api/types/role.go index a36d7242cd86f..25902d8e5c08d 100644 --- a/api/types/role.go +++ b/api/types/role.go @@ -1855,6 +1855,8 @@ func (r *RoleV6) GetLabelMatchers(rct RoleConditionType, kind string) (LabelMatc return LabelMatchers{cond.DatabaseServiceLabels, cond.DatabaseServiceLabelsExpression}, nil case KindWindowsDesktop: return LabelMatchers{cond.WindowsDesktopLabels, cond.WindowsDesktopLabelsExpression}, nil + case KindDynamicWindowsDesktop: + return LabelMatchers{cond.WindowsDesktopLabels, cond.WindowsDesktopLabelsExpression}, nil case KindWindowsDesktopService: return LabelMatchers{cond.WindowsDesktopLabels, cond.WindowsDesktopLabelsExpression}, nil case KindUserGroup: diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 937c89e765a6d..85881e3c026d7 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -247,6 +247,12 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) { if cfg.WindowsDesktops == nil { cfg.WindowsDesktops = local.NewWindowsDesktopService(cfg.Backend) } + if cfg.DynamicWindowsDesktops == nil { + cfg.DynamicWindowsDesktops, err = local.NewDynamicWindowsDesktopService(cfg.Backend) + if err != nil { + return nil, trace.Wrap(err) + } + } if cfg.SAMLIdPServiceProviders == nil { cfg.SAMLIdPServiceProviders, err = local.NewSAMLIdPServiceProviderService(cfg.Backend) if err != nil { @@ -450,6 +456,7 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) { AuditLogSessionStreamer: cfg.AuditLog, Events: cfg.Events, WindowsDesktops: cfg.WindowsDesktops, + DynamicWindowsDesktops: cfg.DynamicWindowsDesktops, SAMLIdPServiceProviders: cfg.SAMLIdPServiceProviders, UserGroups: cfg.UserGroups, SessionTrackerService: cfg.SessionTrackerService, @@ -651,6 +658,7 @@ type Services struct { services.Databases services.DatabaseServices services.WindowsDesktops + services.DynamicWindowsDesktops services.SAMLIdPServiceProviders services.UserGroups services.SessionTrackerService diff --git a/lib/auth/dynamicwindows/dynamicwindowsv1/service.go b/lib/auth/dynamicwindows/dynamicwindowsv1/service.go new file mode 100644 index 0000000000000..98bc2f81e6b55 --- /dev/null +++ b/lib/auth/dynamicwindows/dynamicwindowsv1/service.go @@ -0,0 +1,215 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dynamicwindowsv1 + +import ( + "context" + "log/slog" + + "github.com/gravitational/trace" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/gravitational/teleport" + dynamicwindowspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/dynamicwindows/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/authz" +) + +// Service implements the teleport.trust.v1.TrustService RPC service. +type Service struct { + dynamicwindowspb.UnimplementedDynamicWindowsServiceServer + + authorizer authz.Authorizer + backend Backend + cache Cache + logger *slog.Logger +} + +// ServiceConfig holds configuration options for Service +type ServiceConfig struct { + // Authorizer is the authorizer service which checks access to resources. + Authorizer authz.Authorizer + // Backend will be used for writing the dynamic Windows desktop resources. + Backend Backend + // Cache will be used for reading and writing the dynamic Windows desktop resources. + Cache Cache + // Logger is the logger instance to use. + Logger *slog.Logger +} + +// Backend is the interface used for writing dynamic Windows desktops +type Backend interface { + CreateDynamicWindowsDesktop(context.Context, types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) + UpdateDynamicWindowsDesktop(context.Context, types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) + UpsertDynamicWindowsDesktop(context.Context, types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) + DeleteDynamicWindowsDesktop(ctx context.Context, name string) error +} + +// Cache is the interface used for reading dynamic Windows desktops +type Cache interface { + GetDynamicWindowsDesktop(ctx context.Context, name string) (types.DynamicWindowsDesktop, error) + ListDynamicWindowsDesktops(ctx context.Context, pageSize int, pageToken string) ([]types.DynamicWindowsDesktop, string, error) +} + +// NewService creates new dynamic Windows desktop service +func NewService(cfg ServiceConfig) (*Service, error) { + switch { + case cfg.Backend == nil: + return nil, trace.BadParameter("backend service is required") + case cfg.Cache == nil: + return nil, trace.BadParameter("cache service is required") + case cfg.Authorizer == nil: + return nil, trace.BadParameter("authorizer is required") + } + + if cfg.Logger == nil { + cfg.Logger = slog.With(teleport.ComponentKey, "dynamicwindows") + } + + return &Service{ + authorizer: cfg.Authorizer, + backend: cfg.Backend, + cache: cfg.Cache, + logger: cfg.Logger, + }, nil +} + +// GetDynamicWindowsDesktop returns registered dynamic Windows desktop by name. +func (s *Service) GetDynamicWindowsDesktop(ctx context.Context, request *dynamicwindowspb.GetDynamicWindowsDesktopRequest) (*types.DynamicWindowsDesktopV1, error) { + auth, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := auth.CheckAccessToKind(types.KindDynamicWindowsDesktop, types.VerbRead); err != nil { + return nil, trace.Wrap(err) + } + + if request.GetName() == "" { + return nil, trace.BadParameter("dynamic windows desktop name is required") + } + + d, err := s.cache.GetDynamicWindowsDesktop(ctx, request.GetName()) + if err != nil { + return nil, trace.Wrap(err) + } + + desktop, ok := d.(*types.DynamicWindowsDesktopV1) + if !ok { + return nil, trace.BadParameter("unexpected type %T", d) + } + + return desktop, nil +} + +// ListDynamicWindowsDesktops returns list of dynamic Windows desktops. +func (s *Service) ListDynamicWindowsDesktops(ctx context.Context, request *dynamicwindowspb.ListDynamicWindowsDesktopsRequest) (*dynamicwindowspb.ListDynamicWindowsDesktopsResponse, error) { + auth, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := auth.CheckAccessToKind(types.KindDynamicWindowsDesktop, types.VerbRead, types.VerbList); err != nil { + return nil, trace.Wrap(err) + } + + desktops, next, err := s.cache.ListDynamicWindowsDesktops(ctx, int(request.PageSize), request.PageToken) + if err != nil { + return nil, trace.Wrap(err) + } + + response := &dynamicwindowspb.ListDynamicWindowsDesktopsResponse{ + NextPageToken: next, + } + for _, d := range desktops { + desktop, ok := d.(*types.DynamicWindowsDesktopV1) + if !ok { + return nil, trace.BadParameter("unexpected type %T", d) + } + response.Desktops = append(response.Desktops, desktop) + } + + return response, nil +} + +// CreateDynamicWindowsDesktop registers a new dynamic Windows desktop. +func (s *Service) CreateDynamicWindowsDesktop(ctx context.Context, req *dynamicwindowspb.CreateDynamicWindowsDesktopRequest) (*types.DynamicWindowsDesktopV1, error) { + auth, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := auth.AuthorizeAdminAction(); err != nil { + return nil, trace.Wrap(err) + } + if err := auth.CheckAccessToKind(types.KindDynamicWindowsDesktop, types.VerbCreate); err != nil { + return nil, trace.Wrap(err) + } + d, err := s.backend.CreateDynamicWindowsDesktop(ctx, types.DynamicWindowsDesktop(req.Desktop)) + if err != nil { + return nil, trace.Wrap(err) + } + + createdDesktop, ok := d.(*types.DynamicWindowsDesktopV1) + if !ok { + return nil, trace.BadParameter("unexpected type %T", d) + } + + return createdDesktop, nil +} + +// UpdateDynamicWindowsDesktop updates an existing dynamic Windows desktop. +func (s *Service) UpdateDynamicWindowsDesktop(ctx context.Context, req *dynamicwindowspb.UpdateDynamicWindowsDesktopRequest) (*types.DynamicWindowsDesktopV1, error) { + auth, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := auth.AuthorizeAdminAction(); err != nil { + return nil, trace.Wrap(err) + } + if err := auth.CheckAccessToKind(types.KindDynamicWindowsDesktop, types.VerbUpdate); err != nil { + return nil, trace.Wrap(err) + } + d, err := s.backend.UpdateDynamicWindowsDesktop(ctx, req.Desktop) + if err != nil { + return nil, trace.Wrap(err) + } + + updatedDesktop, ok := d.(*types.DynamicWindowsDesktopV1) + if !ok { + return nil, trace.BadParameter("unexpected type %T", d) + } + + return updatedDesktop, nil +} + +// DeleteDynamicWindowsDesktop removes the specified dynamic Windows desktop. +func (s *Service) DeleteDynamicWindowsDesktop(ctx context.Context, req *dynamicwindowspb.DeleteDynamicWindowsDesktopRequest) (*emptypb.Empty, error) { + auth, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := auth.AuthorizeAdminAction(); err != nil { + return nil, trace.Wrap(err) + } + if err := auth.CheckAccessToKind(types.KindDynamicWindowsDesktop, types.VerbDelete); err != nil { + return nil, trace.Wrap(err) + } + if err := s.backend.DeleteDynamicWindowsDesktop(ctx, req.GetName()); err != nil { + return nil, trace.Wrap(err) + } + return &emptypb.Empty{}, nil +} diff --git a/lib/auth/dynamicwindows/dynamicwindowsv1/service_test.go b/lib/auth/dynamicwindows/dynamicwindowsv1/service_test.go new file mode 100644 index 0000000000000..8fee09af10dfb --- /dev/null +++ b/lib/auth/dynamicwindows/dynamicwindowsv1/service_test.go @@ -0,0 +1,220 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dynamicwindowsv1 + +import ( + "context" + "fmt" + "slices" + "testing" + + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + + dynamicwindowsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dynamicwindows/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/authz" + "github.com/gravitational/teleport/lib/backend/memory" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/services/local" + "github.com/gravitational/teleport/lib/tlsca" + "github.com/gravitational/teleport/lib/utils" +) + +func TestServiceAccess(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + allowedVerbs []string + allowedStates []authz.AdminActionAuthState + }{ + { + name: "CreateDynamicWindowsDesktop", + allowedStates: []authz.AdminActionAuthState{authz.AdminActionAuthNotRequired, authz.AdminActionAuthMFAVerified}, + allowedVerbs: []string{types.VerbCreate}, + }, + { + name: "UpdateDynamicWindowsDesktop", + allowedStates: []authz.AdminActionAuthState{authz.AdminActionAuthNotRequired, authz.AdminActionAuthMFAVerified}, + allowedVerbs: []string{types.VerbUpdate}, + }, + { + name: "DeleteDynamicWindowsDesktop", + allowedStates: []authz.AdminActionAuthState{authz.AdminActionAuthNotRequired, authz.AdminActionAuthMFAVerified}, + allowedVerbs: []string{types.VerbDelete}, + }, + { + name: "ListDynamicWindowsDesktops", + allowedStates: []authz.AdminActionAuthState{ + authz.AdminActionAuthUnauthorized, authz.AdminActionAuthNotRequired, + authz.AdminActionAuthMFAVerified, authz.AdminActionAuthMFAVerifiedWithReuse, + }, + allowedVerbs: []string{types.VerbRead, types.VerbList}, + }, + { + name: "GetDynamicWindowsDesktop", + allowedStates: []authz.AdminActionAuthState{ + authz.AdminActionAuthUnauthorized, authz.AdminActionAuthNotRequired, + authz.AdminActionAuthMFAVerified, authz.AdminActionAuthMFAVerifiedWithReuse, + }, + allowedVerbs: []string{types.VerbRead}, + }, + } + + for _, tt := range testCases { + for _, state := range tt.allowedStates { + for _, verbs := range utils.Combinations(tt.allowedVerbs) { + t.Run(fmt.Sprintf("%v,allowed:%v,verbs:%v", tt.name, stateToString(state), verbs), func(t *testing.T) { + service := newService(t, state, fakeChecker{allowedVerbs: verbs}) + err := callMethod(service, tt.name) + // expect access denied except with full set of verbs. + if len(verbs) == len(tt.allowedVerbs) { + require.False(t, trace.IsAccessDenied(err)) + } else { + require.Error(t, err) + require.True(t, trace.IsAccessDenied(err), "expected access denied for verbs %v, got err=%v", verbs, err) + } + }) + } + } + + disallowedStates := otherAdminStates(tt.allowedStates) + for _, state := range disallowedStates { + t.Run(fmt.Sprintf("%v,disallowed:%v", tt.name, stateToString(state)), func(t *testing.T) { + // it is enough to test against tt.allowedVerbs, + // this is the only different data point compared to the test cases above. + service := newService(t, state, fakeChecker{allowedVerbs: tt.allowedVerbs}) + err := callMethod(service, tt.name) + require.True(t, trace.IsAccessDenied(err)) + }) + } + } + + // verify that all declared methods have matching test cases + for _, method := range dynamicwindowsv1.DynamicWindowsService_ServiceDesc.Methods { + t.Run(fmt.Sprintf("%v covered", method.MethodName), func(t *testing.T) { + match := false + for _, testCase := range testCases { + match = match || testCase.name == method.MethodName + } + require.True(t, match, "method %v without coverage, no matching tests", method.MethodName) + }) + } +} + +var allAdminStates = map[authz.AdminActionAuthState]string{ + authz.AdminActionAuthUnauthorized: "Unauthorized", + authz.AdminActionAuthNotRequired: "NotRequired", + authz.AdminActionAuthMFAVerified: "MFAVerified", + authz.AdminActionAuthMFAVerifiedWithReuse: "MFAVerifiedWithReuse", +} + +func stateToString(state authz.AdminActionAuthState) string { + str, ok := allAdminStates[state] + if !ok { + return fmt.Sprintf("unknown(%v)", state) + } + return str +} + +// otherAdminStates returns all admin states except for those passed in +func otherAdminStates(states []authz.AdminActionAuthState) []authz.AdminActionAuthState { + var out []authz.AdminActionAuthState + for state := range allAdminStates { + found := slices.Index(states, state) != -1 + if !found { + out = append(out, state) + } + } + return out +} + +// callMethod calls a method with given name in the DynamicWindowsDesktop service +func callMethod(service *Service, method string) error { + for _, desc := range dynamicwindowsv1.DynamicWindowsService_ServiceDesc.Methods { + if desc.MethodName == method { + _, err := desc.Handler(service, context.Background(), func(arg any) error { + switch arg := arg.(type) { + case *dynamicwindowsv1.CreateDynamicWindowsDesktopRequest: + arg.Desktop, _ = types.NewDynamicWindowsDesktopV1("test", nil, types.DynamicWindowsDesktopSpecV1{ + Addr: "test", + }) + case *dynamicwindowsv1.UpdateDynamicWindowsDesktopRequest: + arg.Desktop, _ = types.NewDynamicWindowsDesktopV1("test", nil, types.DynamicWindowsDesktopSpecV1{ + Addr: "test", + }) + } + return nil + }, nil) + return err + } + } + return fmt.Errorf("method %v not found", method) +} + +type fakeChecker struct { + allowedVerbs []string + services.AccessChecker +} + +func (f fakeChecker) CheckAccessToRule(_ services.RuleContext, _ string, resource string, verb string) error { + if resource == types.KindDynamicWindowsDesktop { + if slices.Contains(f.allowedVerbs, verb) { + return nil + } + } + + return trace.AccessDenied("access denied to rule=%v/verb=%v", resource, verb) +} + +func newService(t *testing.T, authState authz.AdminActionAuthState, checker services.AccessChecker) *Service { + t.Helper() + + b, err := memory.New(memory.Config{}) + require.NoError(t, err) + + backendService, err := local.NewDynamicWindowsDesktopService(b) + require.NoError(t, err) + + authorizer := authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + user, err := types.NewUser("probakowski") + if err != nil { + return nil, err + } + return &authz.Context{ + User: user, + Checker: checker, + AdminActionAuthState: authState, + Identity: authz.LocalUser{ + Identity: tlsca.Identity{ + Username: user.GetName(), + }, + }, + }, nil + }) + + service, err := NewService(ServiceConfig{ + Authorizer: authorizer, + Backend: backendService, + Cache: backendService, + }) + require.NoError(t, err) + return service +} diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go index a9f25e84f559f..0b10422ff9652 100644 --- a/lib/auth/grpcserver.go +++ b/lib/auth/grpcserver.go @@ -57,6 +57,7 @@ import ( dbobjectv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobject/v1" dbobjectimportrulev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1" discoveryconfigv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/discoveryconfig/v1" + dynamicwindowsv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/dynamicwindows/v1" integrationv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" kubewaitingcontainerv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/kubewaitingcontainer/v1" loginrulev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1" @@ -85,6 +86,7 @@ import ( "github.com/gravitational/teleport/lib/auth/dbobject/dbobjectv1" "github.com/gravitational/teleport/lib/auth/dbobjectimportrule/dbobjectimportrulev1" "github.com/gravitational/teleport/lib/auth/discoveryconfig/discoveryconfigv1" + "github.com/gravitational/teleport/lib/auth/dynamicwindows/dynamicwindowsv1" "github.com/gravitational/teleport/lib/auth/integration/integrationv1" "github.com/gravitational/teleport/lib/auth/kubewaitingcontainer/kubewaitingcontainerv1" "github.com/gravitational/teleport/lib/auth/loginrule/loginrulev1" @@ -5141,6 +5143,17 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) { collectortracepb.RegisterTraceServiceServer(server, authServer) auditlogpb.RegisterAuditLogServiceServer(server, authServer) + dynamicWindows, err := dynamicwindowsv1.NewService(dynamicwindowsv1.ServiceConfig{ + Authorizer: cfg.Authorizer, + Backend: cfg.AuthServer.Services, + // TODO(probakowski): switch to cache when support is added + Cache: cfg.AuthServer.Services, + }) + if err != nil { + return nil, trace.Wrap(err) + } + dynamicwindowsv1pb.RegisterDynamicWindowsServiceServer(server, dynamicWindows) + trust, err := trustv1.NewService(&trustv1.ServiceConfig{ Authorizer: cfg.Authorizer, Cache: cfg.AuthServer.Cache, diff --git a/lib/auth/init.go b/lib/auth/init.go index f56dac7af91db..d7c2309cd2b89 100644 --- a/lib/auth/init.go +++ b/lib/auth/init.go @@ -218,9 +218,12 @@ type InitConfig struct { // session related streams Streamer events.Streamer - // WindowsServices is a service that manages Windows desktop resources. + // WindowsDesktops is a service that manages Windows desktop resources. WindowsDesktops services.WindowsDesktops + // DynamicWindowsServices is a service that manages dynamic Windows desktop resources. + DynamicWindowsDesktops services.DynamicWindowsDesktops + // SAMLIdPServiceProviders is a service that manages SAML IdP service providers. SAMLIdPServiceProviders services.SAMLIdPServiceProviders diff --git a/lib/authz/permissions.go b/lib/authz/permissions.go index f8b5587018608..86375ca1bb5ba 100644 --- a/lib/authz/permissions.go +++ b/lib/authz/permissions.go @@ -1175,6 +1175,7 @@ func definitionForBuiltinRole(clusterName string, recConfig readonly.SessionReco types.NewRule(types.KindLock, services.RO()), types.NewRule(types.KindWindowsDesktopService, services.RW()), types.NewRule(types.KindWindowsDesktop, services.RW()), + types.NewRule(types.KindDynamicWindowsDesktop, services.RW()), }, }, }) diff --git a/lib/services/dynamic_desktop.go b/lib/services/dynamic_desktop.go new file mode 100644 index 0000000000000..5903e4a88b79f --- /dev/null +++ b/lib/services/dynamic_desktop.go @@ -0,0 +1,90 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package services + +import ( + "context" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/utils" +) + +// DynamicWindowsDesktops defines an interface for managing dynamic Windows desktops. +type DynamicWindowsDesktops interface { + GetDynamicWindowsDesktop(ctx context.Context, name string) (types.DynamicWindowsDesktop, error) + CreateDynamicWindowsDesktop(context.Context, types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) + UpdateDynamicWindowsDesktop(context.Context, types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) + UpsertDynamicWindowsDesktop(context.Context, types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) + DeleteDynamicWindowsDesktop(ctx context.Context, name string) error + ListDynamicWindowsDesktops(ctx context.Context, pageSize int, pageToken string) ([]types.DynamicWindowsDesktop, string, error) +} + +// MarshalDynamicWindowsDesktop marshals the DynamicWindowsDesktop resource to JSON. +func MarshalDynamicWindowsDesktop(s types.DynamicWindowsDesktop, opts ...MarshalOption) ([]byte, error) { + cfg, err := CollectOptions(opts) + if err != nil { + return nil, trace.Wrap(err) + } + + switch s := s.(type) { + case *types.DynamicWindowsDesktopV1: + if err := s.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + return utils.FastMarshal(maybeResetProtoRevision(cfg.PreserveRevision, s)) + default: + return nil, trace.BadParameter("unrecognized windows desktop version %T", s) + } +} + +// UnmarshalDynamicWindowsDesktop unmarshals the DynamicWindowsDesktop resource from JSON. +func UnmarshalDynamicWindowsDesktop(data []byte, opts ...MarshalOption) (types.DynamicWindowsDesktop, error) { + if len(data) == 0 { + return nil, trace.BadParameter("missing windows desktop data") + } + cfg, err := CollectOptions(opts) + if err != nil { + return nil, trace.Wrap(err) + } + var h types.ResourceHeader + if err := utils.FastUnmarshal(data, &h); err != nil { + return nil, trace.Wrap(err) + } + switch h.Version { + case types.V1: + var s types.DynamicWindowsDesktopV1 + if err := utils.FastUnmarshal(data, &s); err != nil { + return nil, trace.BadParameter(err.Error()) + } + if err := s.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + if cfg.Revision != "" { + s.SetRevision(cfg.Revision) + } + if !cfg.Expires.IsZero() { + s.SetExpiry(cfg.Expires) + } + return &s, nil + } + return nil, trace.BadParameter("windows desktop resource version %q is not supported", h.Version) +} diff --git a/lib/services/local/dynamic_desktops.go b/lib/services/local/dynamic_desktops.go new file mode 100644 index 0000000000000..b4b482d600de7 --- /dev/null +++ b/lib/services/local/dynamic_desktops.go @@ -0,0 +1,119 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package local + +import ( + "context" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/services/local/generic" +) + +// DynamicWindowsDesktopService manages dynamic Windows desktop resources in the backend. +type DynamicWindowsDesktopService struct { + service *generic.Service[types.DynamicWindowsDesktop] +} + +// NewDynamicWindowsDesktopService creates a new WindowsDesktopsService. +func NewDynamicWindowsDesktopService(b backend.Backend) (*DynamicWindowsDesktopService, error) { + service, err := generic.NewService(&generic.ServiceConfig[types.DynamicWindowsDesktop]{ + Backend: b, + ResourceKind: types.KindDynamicWindowsDesktop, + PageLimit: defaults.MaxIterationLimit, + BackendPrefix: backend.NewKey(dynamicWindowsDesktopsPrefix), + MarshalFunc: services.MarshalDynamicWindowsDesktop, + UnmarshalFunc: services.UnmarshalDynamicWindowsDesktop, + }) + if err != nil { + return nil, trace.Wrap(err) + } + return &DynamicWindowsDesktopService{ + service: service, + }, nil +} + +// GetDynamicWindowsDesktop returns dynamic Windows desktops by name. +func (s *DynamicWindowsDesktopService) GetDynamicWindowsDesktop(ctx context.Context, name string) (types.DynamicWindowsDesktop, error) { + desktop, err := s.service.GetResource(ctx, name) + if err != nil { + return nil, trace.Wrap(err) + } + return desktop, err +} + +// CreateDynamicWindowsDesktop creates a dynamic Windows desktop resource. +func (s *DynamicWindowsDesktopService) CreateDynamicWindowsDesktop(ctx context.Context, desktop types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) { + d, err := s.service.CreateResource(ctx, desktop) + if err != nil { + return nil, trace.Wrap(err) + } + return d, err +} + +// UpdateDynamicWindowsDesktop updates a dynamic Windows desktop resource. +func (s *DynamicWindowsDesktopService) UpdateDynamicWindowsDesktop(ctx context.Context, desktop types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) { + d, err := s.service.UpdateResource(ctx, desktop) + if err != nil { + return nil, trace.Wrap(err) + } + return d, err +} + +// UpsertDynamicWindowsDesktop updates a dynamic Windows desktop resource, creating it if it doesn't exist. +func (s *DynamicWindowsDesktopService) UpsertDynamicWindowsDesktop(ctx context.Context, desktop types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) { + d, err := s.service.UpsertResource(ctx, desktop) + if err != nil { + return nil, trace.Wrap(err) + } + return d, err +} + +// DeleteDynamicWindowsDesktop removes the specified dynamic Windows desktop resource. +func (s *DynamicWindowsDesktopService) DeleteDynamicWindowsDesktop(ctx context.Context, name string) error { + if err := s.service.DeleteResource(ctx, name); err != nil { + return trace.Wrap(err) + } + return nil +} + +// DeleteAllDynamicWindowsDesktops removes all dynamic Windows desktop resources. +func (s *DynamicWindowsDesktopService) DeleteAllDynamicWindowsDesktops(ctx context.Context) error { + if err := s.service.DeleteAllResources(ctx); err != nil { + return trace.Wrap(err) + } + return nil +} + +// ListDynamicWindowsDesktops returns all dynamic Windows desktops matching filter. +func (s *DynamicWindowsDesktopService) ListDynamicWindowsDesktops(ctx context.Context, pageSize int, pageToken string) ([]types.DynamicWindowsDesktop, string, error) { + desktops, next, err := s.service.ListResources(ctx, pageSize, pageToken) + if err != nil { + return nil, "", trace.Wrap(err) + } + return desktops, next, nil +} + +const ( + dynamicWindowsDesktopsPrefix = "dynamicWindowsDesktop" +) diff --git a/lib/services/local/dynamic_desktops_test.go b/lib/services/local/dynamic_desktops_test.go new file mode 100644 index 0000000000000..75ed040080648 --- /dev/null +++ b/lib/services/local/dynamic_desktops_test.go @@ -0,0 +1,232 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package local + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/backend/memory" +) + +func newDynamicDesktop(t *testing.T, name string) types.DynamicWindowsDesktop { + desktop, err := types.NewDynamicWindowsDesktopV1(name, nil, types.DynamicWindowsDesktopSpecV1{ + Addr: "xyz", + }) + require.NoError(t, err) + return desktop +} + +func setupDynamicDesktopTest(t *testing.T) (context.Context, *DynamicWindowsDesktopService) { + ctx := context.Background() + clock := clockwork.NewFakeClock() + mem, err := memory.New(memory.Config{ + Context: ctx, + Clock: clock, + }) + require.NoError(t, err) + service, err := NewDynamicWindowsDesktopService(backend.NewSanitizer(mem)) + require.NoError(t, err) + return ctx, service +} + +func TestDynamicWindowsService_CreateDynamicDesktop(t *testing.T) { + t.Parallel() + ctx, service := setupDynamicDesktopTest(t) + t.Run("ok", func(t *testing.T) { + want := newDynamicDesktop(t, "example") + got, err := service.CreateDynamicWindowsDesktop(ctx, want.Copy()) + want.SetRevision(got.GetRevision()) + require.NoError(t, err) + require.NotEmpty(t, got.GetRevision()) + require.Empty(t, cmp.Diff( + want, + got, + protocmp.Transform(), + )) + }) + t.Run("no upsert", func(t *testing.T) { + want := newDynamicDesktop(t, "upsert") + _, err := service.CreateDynamicWindowsDesktop(ctx, want.Copy()) + require.NoError(t, err) + _, err = service.CreateDynamicWindowsDesktop(ctx, want.Copy()) + require.Error(t, err) + require.True(t, trace.IsAlreadyExists(err)) + }) +} + +func TestDynamicWindowsService_UpsertDynamicDesktop(t *testing.T) { + ctx, service := setupDynamicDesktopTest(t) + want := newDynamicDesktop(t, "example") + got, err := service.UpsertDynamicWindowsDesktop(ctx, want.Copy()) + want.SetRevision(got.GetRevision()) + require.NoError(t, err) + require.NotEmpty(t, got.GetRevision()) + require.Empty(t, cmp.Diff( + want, + got, + protocmp.Transform(), + )) + _, err = service.UpsertDynamicWindowsDesktop(ctx, want.Copy()) + require.NoError(t, err) +} + +func TestDynamicWindowsService_GetDynamicDesktop(t *testing.T) { + t.Parallel() + ctx, service := setupDynamicDesktopTest(t) + t.Run("not found", func(t *testing.T) { + _, err := service.GetDynamicWindowsDesktop(ctx, "notfound") + require.Error(t, err) + require.True(t, trace.IsNotFound(err)) + }) + t.Run("ok", func(t *testing.T) { + want := newDynamicDesktop(t, "example") + created, err := service.CreateDynamicWindowsDesktop(ctx, want.Copy()) + require.NoError(t, err) + got, err := service.GetDynamicWindowsDesktop(ctx, "example") + require.NoError(t, err) + require.Empty(t, cmp.Diff( + created, + got, + protocmp.Transform(), + )) + }) +} + +func TestDynamicWindowsService_ListDynamicDesktop(t *testing.T) { + t.Parallel() + t.Run("none", func(t *testing.T) { + ctx, service := setupDynamicDesktopTest(t) + desktops, _, err := service.ListDynamicWindowsDesktops(ctx, 5, "") + require.NoError(t, err) + require.Empty(t, desktops) + }) + t.Run("list all", func(t *testing.T) { + ctx, service := setupDynamicDesktopTest(t) + d1, err := service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "d1")) + require.NoError(t, err) + d2, err := service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "d2")) + require.NoError(t, err) + desktops, next, err := service.ListDynamicWindowsDesktops(ctx, 5, "") + require.NoError(t, err) + require.Len(t, desktops, 2) + require.Empty(t, next) + require.Empty(t, cmp.Diff( + d1, + desktops[0], + protocmp.Transform(), + )) + require.Empty(t, cmp.Diff( + d2, + desktops[1], + protocmp.Transform(), + )) + }) + t.Run("list paged", func(t *testing.T) { + ctx, service := setupDynamicDesktopTest(t) + d1, err := service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "d1")) + require.NoError(t, err) + d2, err := service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "d2")) + require.NoError(t, err) + desktops, next, err := service.ListDynamicWindowsDesktops(ctx, 1, "") + require.NoError(t, err) + require.Len(t, desktops, 1) + require.NotEmpty(t, next) + require.Empty(t, cmp.Diff( + d1, + desktops[0], + protocmp.Transform(), + )) + desktops, next, err = service.ListDynamicWindowsDesktops(ctx, 1, next) + require.NoError(t, err) + require.Len(t, desktops, 1) + require.Empty(t, next) + require.Empty(t, cmp.Diff( + d2, + desktops[0], + protocmp.Transform(), + )) + }) +} + +func TestDynamicWindowsService_UpdateDynamicDesktop(t *testing.T) { + t.Parallel() + ctx, service := setupDynamicDesktopTest(t) + t.Run("not found", func(t *testing.T) { + want := newDynamicDesktop(t, "example") + _, err := service.UpdateDynamicWindowsDesktop(ctx, want.Copy()) + require.Error(t, err) + require.True(t, trace.IsNotFound(err)) + }) + t.Run("ok", func(t *testing.T) { + want := newDynamicDesktop(t, "example") + created, err := service.CreateDynamicWindowsDesktop(ctx, want.Copy()) + require.NoError(t, err) + updated, err := service.UpdateDynamicWindowsDesktop(ctx, created.Copy()) + require.NoError(t, err) + require.NotEqual(t, created.GetRevision(), updated.GetRevision()) + }) +} + +func TestDynamicWindowsService_DeleteDynamicDesktop(t *testing.T) { + t.Parallel() + ctx, service := setupDynamicDesktopTest(t) + t.Run("not found", func(t *testing.T) { + err := service.DeleteDynamicWindowsDesktop(ctx, "notfound") + require.Error(t, err) + require.True(t, trace.IsNotFound(err)) + }) + t.Run("ok", func(t *testing.T) { + _, err := service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "example")) + require.NoError(t, err) + _, err = service.GetDynamicWindowsDesktop(ctx, "example") + require.NoError(t, err) + err = service.DeleteDynamicWindowsDesktop(ctx, "example") + require.NoError(t, err) + _, err = service.GetDynamicWindowsDesktop(ctx, "example") + require.Error(t, err) + require.True(t, trace.IsNotFound(err)) + }) +} + +func TestDynamicWindowsService_DeleteAllDynamicDesktop(t *testing.T) { + ctx, service := setupDynamicDesktopTest(t) + err := service.DeleteAllDynamicWindowsDesktops(ctx) + require.NoError(t, err) + _, err = service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "d1")) + require.NoError(t, err) + _, err = service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "d2")) + require.NoError(t, err) + desktops, _, err := service.ListDynamicWindowsDesktops(ctx, 5, "") + require.NoError(t, err) + require.Len(t, desktops, 2) + err = service.DeleteAllDynamicWindowsDesktops(ctx) + require.NoError(t, err) + desktops, _, err = service.ListDynamicWindowsDesktops(ctx, 5, "") + require.NoError(t, err) + require.Empty(t, desktops) +} diff --git a/lib/services/resource.go b/lib/services/resource.go index c336f1b80f1d3..819ec724cdc81 100644 --- a/lib/services/resource.go +++ b/lib/services/resource.go @@ -189,6 +189,8 @@ func ParseShortcut(in string) (string, error) { return types.KindWindowsDesktopService, nil case types.KindWindowsDesktop, "win_desktop": return types.KindWindowsDesktop, nil + case types.KindDynamicWindowsDesktop, "dynamic_win_desktop", "dynamic_desktop": + return types.KindDynamicWindowsDesktop, nil case types.KindToken, "tokens": return types.KindToken, nil case types.KindInstaller: diff --git a/lib/services/role.go b/lib/services/role.go index 0d877761bbced..083b277c9b81f 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -73,6 +73,7 @@ var DefaultImplicitRules = []types.Rule{ types.NewRule(types.KindApp, RO()), types.NewRule(types.KindWindowsDesktopService, RO()), types.NewRule(types.KindWindowsDesktop, RO()), + types.NewRule(types.KindDynamicWindowsDesktop, RO()), types.NewRule(types.KindKubernetesCluster, RO()), types.NewRule(types.KindUsageEvent, []string{types.VerbCreate}), types.NewRule(types.KindVnetConfig, RO()), From 0dd0ac22acdbb2d4cc64cd4093e67da93d9bcc3a Mon Sep 17 00:00:00 2001 From: Przemko Robakowski Date: Tue, 22 Oct 2024 22:42:50 +0200 Subject: [PATCH 61/61] fix loggers --- lib/services/watcher.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/services/watcher.go b/lib/services/watcher.go index 663f0c96dbb25..93daf0aee5cd6 100644 --- a/lib/services/watcher.go +++ b/lib/services/watcher.go @@ -1086,7 +1086,7 @@ func (p *dynamicWindowsDesktopCollector) processEventsAndUpdateCurrent(ctx conte var updated bool for _, event := range events { if event.Resource == nil || event.Resource.GetKind() != types.KindDynamicWindowsDesktop { - p.Log.Warnf("Unexpected event: %v.", event) + p.Logger.WarnContext(ctx, "Received unexpected event", "event", logutils.StringerAttr(event)) continue } switch event.Type { @@ -1096,13 +1096,13 @@ func (p *dynamicWindowsDesktopCollector) processEventsAndUpdateCurrent(ctx conte case types.OpPut: dynamicWindowsDesktop, ok := event.Resource.(types.DynamicWindowsDesktop) if !ok { - p.Log.Warnf("Unexpected resource type %T.", event.Resource) + p.Logger.WarnContext(ctx, "Received unexpected resource type", "resource", event.Resource.GetKind()) continue } p.current[dynamicWindowsDesktop.GetName()] = dynamicWindowsDesktop updated = true default: - p.Log.Warnf("Unsupported event type %s.", event.Type) + p.Logger.WarnContext(ctx, "Received unsupported event type", "event_type", event.Type) } }