Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add labels to Windows Desktop Service, add endpoint for searching them #16436

Merged
merged 12 commits into from
Sep 23, 2022
Merged
2 changes: 2 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2844,6 +2844,8 @@ func (c *Client) ListResources(ctx context.Context, req proto.ListResourcesReque
resources[i] = respResource.GetKubeService()
case types.KindWindowsDesktop:
resources[i] = respResource.GetWindowsDesktop()
case types.KindWindowsDesktopService:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would check with Lisa on this. I don't remember the details but there is some reason why ListResources isn't supported for desktops.

resources[i] = respResource.GetWindowsDesktopService()
case types.KindKubernetesCluster:
resources[i] = respResource.GetKubeCluster()
case types.KindKubeServer:
Expand Down
1,547 changes: 814 additions & 733 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions api/proto/teleport/legacy/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,8 @@ message PaginatedResource {
types.KubernetesClusterV3 KubeCluster = 6 [(gogoproto.jsontag) = "kube_cluster,omitempty"];
// KubernetesServer represents a Kubernetes Server resource.
types.KubernetesServerV3 KubernetesServer = 7 [(gogoproto.jsontag) = "kubernetes_server,omitempty"];
// WindowsDesktopService represents a WindowsDesktopServiceV3 resource.
types.WindowsDesktopServiceV3 WindowsDesktopService = 8 [(gogoproto.jsontag) = "windows_desktop_service,omitempty"];
}
}

Expand Down
34 changes: 29 additions & 5 deletions api/types/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,24 @@ type WindowsDesktopService interface {
ProxiedService
}

type WindowsDesktopServices []WindowsDesktopService

// AsResources returns windows desktops as type resources with labels.
func (s WindowsDesktopServices) AsResources() []ResourceWithLabels {
resources := make([]ResourceWithLabels, 0, len(s))
for _, server := range s {
resources = append(resources, ResourceWithLabels(server))
}
return resources
}

var _ WindowsDesktopService = &WindowsDesktopServiceV3{}

// NewWindowsDesktopServiceV3 creates a new WindowsDesktopServiceV3 resource.
func NewWindowsDesktopServiceV3(name string, spec WindowsDesktopServiceSpecV3) (*WindowsDesktopServiceV3, error) {
func NewWindowsDesktopServiceV3(meta Metadata, spec WindowsDesktopServiceSpecV3) (*WindowsDesktopServiceV3, error) {
s := &WindowsDesktopServiceV3{
ResourceHeader: ResourceHeader{
Metadata: Metadata{
Name: name,
},
Metadata: meta,
},
Spec: spec,
}
Expand Down Expand Up @@ -129,7 +138,8 @@ func (s *WindowsDesktopServiceV3) GetHostname() string {
// MatchSearch goes through select field values and tries to
// match against the list of search values.
func (s *WindowsDesktopServiceV3) MatchSearch(values []string) bool {
return MatchSearch(nil, values, nil)
fieldVals := append(utils.MapToStrings(s.GetAllLabels()), s.GetName(), s.GetHostname())
return MatchSearch(fieldVals, values, nil)
}

// WindowsDesktop represents a Windows desktop host.
Expand Down Expand Up @@ -346,3 +356,17 @@ type ListWindowsDesktopsRequest struct {
Labels map[string]string
SearchKeywords []string
}

// ListWindowsDesktopServicesResponse is a response type to ListWindowsDesktopServices.
type ListWindowsDesktopServicesResponse struct {
DesktopServices []WindowsDesktopService
NextKey string
}

// ListWindowsDesktopServicesRequest is a request type to ListWindowsDesktopServices.
type ListWindowsDesktopServicesRequest struct {
Limit int
StartKey, PredicateExpression string
Labels map[string]string
SearchKeywords []string
}
13 changes: 13 additions & 0 deletions api/types/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,19 @@ func (r ResourcesWithLabels) AsWindowsDesktops() ([]WindowsDesktop, error) {
return desktops, nil
}

// AsWindowsDesktopServices converts each resource into type WindowsDesktop.
func (r ResourcesWithLabels) AsWindowsDesktopServices() ([]WindowsDesktopService, error) {
desktopServices := make([]WindowsDesktopService, 0, len(r))
for _, resource := range r {
desktopService, ok := resource.(WindowsDesktopService)
if !ok {
return nil, trace.BadParameter("expected types.WindowsDesktopService, got: %T", resource)
}
desktopServices = append(desktopServices, desktopService)
}
return desktopServices, nil
}

// AsKubeClusters converts each resource into type KubeCluster.
func (r ResourcesWithLabels) AsKubeClusters() ([]KubeCluster, error) {
clusters := make([]KubeCluster, 0, len(r))
Expand Down
4 changes: 3 additions & 1 deletion api/types/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,9 @@ func TestMatchSearch_ResourceSpecific(t *testing.T) {
name: "desktop service",
searchNotDefined: true,
newResource: func() ResourceWithLabels {
desktopService, err := NewWindowsDesktopServiceV3("_", WindowsDesktopServiceSpecV3{
desktopService, err := NewWindowsDesktopServiceV3(Metadata{
Name: "foo",
}, WindowsDesktopServiceSpecV3{
Addr: "_",
TeleportVersion: "_",
})
Expand Down
7 changes: 6 additions & 1 deletion docs/pages/includes/desktop-access/desktop-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,9 @@ windows_desktop_service:
environment: dev
- match: '^.*\.prod\.example\.com$'
labels:
environment: prod
environment: prod

# Labels to attach to the Windows Desktop Service. This is used internally, so
# any custom labels added won't affect the Windows hosts.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the "This is used internally" sentence means. Since this field is only used internally, when would someone want to set a custom value for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They shouldn't, but I didn't know if we allowed for config properties that are only used internally to go undocumented on the config reference page.

labels:
teleport.internal/resource-id: "resource-id"
2 changes: 2 additions & 0 deletions lib/auth/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,8 @@ type Cache interface {
ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error)
// ListWindowsDesktops returns a paginated list of windows desktops.
ListWindowsDesktops(ctx context.Context, req types.ListWindowsDesktopsRequest) (*types.ListWindowsDesktopsResponse, error)
// ListWindowsDesktopServices returns a paginated list of windows desktops.
ListWindowsDesktopServices(ctx context.Context, req types.ListWindowsDesktopServicesRequest) (*types.ListWindowsDesktopServicesResponse, error)

// GetInstaller gets installer resource for this cluster
GetInstaller(ctx context.Context, name string) (types.Installer, error)
Expand Down
16 changes: 16 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3208,6 +3208,22 @@ func (a *Server) ListResources(ctx context.Context, req proto.ListResourcesReque
NextKey: wResp.NextKey,
}, nil
}
if req.ResourceType == types.KindWindowsDesktopService {
wResp, err := a.ListWindowsDesktopServices(ctx, types.ListWindowsDesktopServicesRequest{
Limit: int(req.Limit),
StartKey: req.StartKey,
PredicateExpression: req.PredicateExpression,
Labels: req.Labels,
SearchKeywords: req.SearchKeywords,
})
if err != nil {
return nil, trace.Wrap(err)
}
return &types.ListResourcesResponse{
Resources: types.WindowsDesktopServices(wResp.DesktopServices).AsResources(),
NextKey: wResp.NextKey,
}, nil
}
return a.Cache.ListResources(ctx, req)
}

Expand Down
13 changes: 10 additions & 3 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -1178,7 +1178,7 @@ func (a *ServerWithRoles) ListResources(ctx context.Context, req proto.ListResou
// https://github.com/gravitational/teleport/pull/1224
actionVerbs = []string{types.VerbList}

case types.KindDatabaseServer, types.KindAppServer, types.KindKubeService, types.KindKubeServer, types.KindWindowsDesktop:
case types.KindDatabaseServer, types.KindAppServer, types.KindKubeService, types.KindKubeServer, types.KindWindowsDesktop, types.KindWindowsDesktopService:

default:
return nil, trace.NotImplemented("resource type %s does not support pagination", req.ResourceType)
Expand Down Expand Up @@ -1263,9 +1263,11 @@ func (r resourceChecker) CanAccess(resource types.Resource) error {
return r.CheckAccess(rr.GetDatabase(), mfaParams)
case types.Database:
return r.CheckAccess(rr, mfaParams)
case types.Server:
return r.CheckAccess(rr, mfaParams)
case types.WindowsDesktop:
return r.CheckAccess(rr, mfaParams)
case types.Server:
case types.WindowsDesktopService:
return r.CheckAccess(rr, mfaParams)
default:
return trace.BadParameter("could not check access to resource type %T", r)
Expand Down Expand Up @@ -1355,7 +1357,7 @@ func (k *kubeChecker) canAccessKubernetes(server types.KubeServer) error {
// newResourceAccessChecker creates a resourceAccessChecker for the provided resource type
func (a *ServerWithRoles) newResourceAccessChecker(resource string) (resourceAccessChecker, error) {
switch resource {
case types.KindAppServer, types.KindDatabaseServer, types.KindWindowsDesktop, types.KindNode:
case types.KindAppServer, types.KindDatabaseServer, types.KindWindowsDesktop, types.KindWindowsDesktopService, types.KindNode:
return &resourceChecker{AccessChecker: a.context.Checker}, nil
case types.KindKubeService, types.KindKubeServer:
return newKubeChecker(a.context), nil
Expand Down Expand Up @@ -1467,6 +1469,11 @@ func (a *ServerWithRoles) ListWindowsDesktops(ctx context.Context, req types.Lis
return nil, trace.NotImplemented(notImplementedMessage)
}

// ListWindowsDesktopServices not implemented: can only be called locally.
func (a *ServerWithRoles) ListWindowsDesktopServices(ctx context.Context, req types.ListWindowsDesktopServicesRequest) (*types.ListWindowsDesktopServicesResponse, error) {
return nil, trace.NotImplemented(notImplementedMessage)
}

func (a *ServerWithRoles) UpsertAuthServer(s types.Server) error {
if err := a.action(apidefaults.Namespace, types.KindAuthServer, types.VerbCreate, types.VerbUpdate); err != nil {
return trace.Wrap(err)
Expand Down
5 changes: 5 additions & 0 deletions lib/auth/clt.go
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,11 @@ func (c *Client) ListWindowsDesktops(ctx context.Context, req types.ListWindowsD
return nil, trace.NotImplemented(notImplementedMessage)
}

// ListWindowsDesktopServices not implemented: can only be called locally.
func (c *Client) ListWindowsDesktopServices(ctx context.Context, req types.ListWindowsDesktopServicesRequest) (*types.ListWindowsDesktopServicesResponse, error) {
return nil, trace.NotImplemented(notImplementedMessage)
}

// DeleteAllUsers not implemented: can only be called locally.
func (c *Client) DeleteAllUsers() error {
return trace.NotImplemented(notImplementedMessage)
Expand Down
7 changes: 7 additions & 0 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -4044,6 +4044,13 @@ func (g *GRPCServer) ListResources(ctx context.Context, req *proto.ListResources
}

protoResource = &proto.PaginatedResource{Resource: &proto.PaginatedResource_WindowsDesktop{WindowsDesktop: desktop}}
case types.KindWindowsDesktopService:
desktopService, ok := resource.(*types.WindowsDesktopServiceV3)
if !ok {
return nil, trace.BadParameter("windows desktop service has invalid type %T", resource)
}

protoResource = &proto.PaginatedResource{Resource: &proto.PaginatedResource_WindowsDesktopService{WindowsDesktopService: desktopService}}
case types.KindKubernetesCluster:
cluster, ok := resource.(*types.KubernetesClusterV3)
if !ok {
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/join_ec2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@ func TestHostUniqueCheck(t *testing.T) {
{
role: types.RoleWindowsDesktop,
upserter: func(name string) {
wds, err := types.NewWindowsDesktopServiceV3(instance1.account+"-"+instance1.instanceID,
wds, err := types.NewWindowsDesktopServiceV3(types.Metadata{Name: instance1.account + "-" + instance1.instanceID},
types.WindowsDesktopServiceSpecV3{
Addr: "localhost:3028",
TeleportVersion: "10.2.2",
Expand Down
13 changes: 13 additions & 0 deletions lib/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2159,6 +2159,19 @@ func (c *Cache) ListWindowsDesktops(ctx context.Context, req types.ListWindowsDe
return rg.windowsDesktops.ListWindowsDesktops(ctx, req)
}

// ListWindowsDesktopServices returns all registered Windows desktop hosts.
func (c *Cache) ListWindowsDesktopServices(ctx context.Context, req types.ListWindowsDesktopServicesRequest) (*types.ListWindowsDesktopServicesResponse, error) {
ctx, span := c.Tracer.Start(ctx, "cache/ListWindowsDesktopServices")
defer span.End()

rg, err := c.read()
if err != nil {
return nil, trace.Wrap(err)
}
defer rg.Release()
return rg.windowsDesktops.ListWindowsDesktopServices(ctx, req)
}

// ListResources is a part of auth.Cache implementation
func (c *Cache) ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error) {
ctx, span := c.Tracer.Start(ctx, "cache/ListResources")
Expand Down
7 changes: 7 additions & 0 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,13 @@ func applyWindowsDesktopConfig(fc *FileConfig, cfg *service.Config) error {
})
}

if fc.WindowsDesktop.Labels != nil {
cfg.WindowsDesktop.Labels = make(map[string]string)
for k, v := range fc.WindowsDesktop.Labels {
cfg.WindowsDesktop.Labels[k] = v
}
}

return nil
}

Expand Down
2 changes: 2 additions & 0 deletions lib/config/fileconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -1698,6 +1698,8 @@ func (m *Metrics) MTLSEnabled() bool {
// WindowsDesktopService contains configuration for windows_desktop_service.
type WindowsDesktopService struct {
Service `yaml:",inline"`
// Labels are the configured windows deesktops service labels.
Labels map[string]string `yaml:"labels,omitempty"`
// PublicAddr is a list of advertised public addresses of this service.
PublicAddr apiutils.Strings `yaml:"public_addr,omitempty"`
// LDAP is the LDAP connection parameters.
Expand Down
1 change: 1 addition & 0 deletions lib/service/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,7 @@ type WindowsDesktopConfig struct {
ConnLimiter limiter.Config
// HostLabels specifies rules that are used to apply labels to Windows hosts.
HostLabels HostLabelRules
Labels map[string]string
}

type LDAPDiscoveryConfig struct {
Expand Down
1 change: 1 addition & 0 deletions lib/service/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ func (process *TeleportProcess) initWindowsDesktopServiceRegistered(log *logrus.
ConnLimiter: connLimiter,
LockWatcher: lockWatcher,
AuthClient: conn.Client,
Labels: cfg.WindowsDesktop.Labels,
HostLabelsFn: cfg.WindowsDesktop.HostLabels.LabelsForHost,
Heartbeat: desktop.HeartbeatConfig{
HostUUID: cfg.HostUUID,
Expand Down
1 change: 1 addition & 0 deletions lib/services/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type WindowsDesktops interface {
DeleteWindowsDesktop(ctx context.Context, hostID, name string) error
DeleteAllWindowsDesktops(context.Context) error
ListWindowsDesktops(ctx context.Context, req types.ListWindowsDesktopsRequest) (*types.ListWindowsDesktopsResponse, error)
ListWindowsDesktopServices(ctx context.Context, req types.ListWindowsDesktopServicesRequest) (*types.ListWindowsDesktopServicesResponse, error)
}

// MarshalWindowsDesktop marshals the WindowsDesktop resource to JSON.
Expand Down
56 changes: 56 additions & 0 deletions lib/services/local/desktops.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,62 @@ func (s *WindowsDesktopService) ListWindowsDesktops(ctx context.Context, req typ
}, nil
}

func (s *WindowsDesktopService) ListWindowsDesktopServices(ctx context.Context, req types.ListWindowsDesktopServicesRequest) (*types.ListWindowsDesktopServicesResponse, error) {
reqLimit := req.Limit
if reqLimit <= 0 {
return nil, trace.BadParameter("nonpositive parameter limit")
}

rangeStart := backend.Key(windowsDesktopServicesPrefix, req.StartKey)
rangeEnd := backend.RangeEnd(backend.Key(windowsDesktopServicesPrefix, ""))
filter := services.MatchResourceFilter{
ResourceKind: types.KindWindowsDesktopService,
Labels: req.Labels,
SearchKeywords: req.SearchKeywords,
PredicateExpression: req.PredicateExpression,
}

// Get most limit+1 results to determine if there will be a next key.
maxLimit := reqLimit + 1
var desktopServices []types.WindowsDesktopService
if err := backend.IterateRange(ctx, s.Backend, rangeStart, rangeEnd, maxLimit, func(items []backend.Item) (stop bool, err error) {
for _, item := range items {
if len(desktopServices) == maxLimit {
break
}

desktop, err := services.UnmarshalWindowsDesktopService(item.Value,
services.WithResourceID(item.ID), services.WithExpires(item.Expires))
if err != nil {
return false, trace.Wrap(err)
}

switch match, err := services.MatchResourceByFilters(desktop, filter, nil /* ignore dup matches */); {
case err != nil:
return false, trace.Wrap(err)
case match:
desktopServices = append(desktopServices, desktop)
}
}

return len(desktopServices) == maxLimit, nil
}); err != nil {
return nil, trace.Wrap(err)
}

var nextKey string
if len(desktopServices) > reqLimit {
nextKey = backend.GetPaginationKey(desktopServices[len(desktopServices)-1])
// Truncate the last item that was used to determine next row existence.
desktopServices = desktopServices[:reqLimit]
}

return &types.ListWindowsDesktopServicesResponse{
DesktopServices: desktopServices,
NextKey: nextKey,
}, nil
}

const (
windowsDesktopsPrefix = "windowsDesktop"
)
3 changes: 3 additions & 0 deletions lib/services/local/presence.go
Original file line number Diff line number Diff line change
Expand Up @@ -1614,6 +1614,9 @@ func (s *PresenceService) listResources(ctx context.Context, req proto.ListResou
case types.KindNode:
keyPrefix = []string{nodesPrefix, req.Namespace}
unmarshalItemFunc = backendItemToServer(types.KindNode)
case types.KindWindowsDesktopService:
keyPrefix = []string{windowsDesktopServicesPrefix, req.Namespace}
unmarshalItemFunc = backendItemToServer(types.KindWindowsDesktopService)
case types.KindKubeService:
keyPrefix = []string{kubeServicesPrefix}
unmarshalItemFunc = backendItemToServer(types.KindKubeService)
Expand Down
2 changes: 1 addition & 1 deletion lib/services/matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func MatchResourceByFilters(resource types.ResourceWithLabels, filter MatchResou
// the user is wanting to filter the contained resource ie. KubeClusters, Application, and Database.
resourceKey := ResourceSeenKey{}
switch filter.ResourceKind {
case types.KindNode, types.KindWindowsDesktop, types.KindKubernetesCluster:
case types.KindNode, types.KindWindowsDesktop, types.KindWindowsDesktopService, types.KindKubernetesCluster:
specResource = resource
resourceKey.name = specResource.GetName()

Expand Down
2 changes: 2 additions & 0 deletions lib/services/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -1999,6 +1999,8 @@ func (set RoleSet) checkAccess(r AccessCheckable, mfa AccessMFAParams, matchers
case types.KindWindowsDesktop:
getRoleLabels = types.Role.GetWindowsDesktopLabels
additionalDeniedMessage = "Confirm Windows user."
case types.KindWindowsDesktopService:
getRoleLabels = types.Role.GetWindowsDesktopLabels
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm is this right? This looks like it only applies to the actual resources, not the Teleport agents that report them.

default:
return trace.BadParameter("cannot match labels for kind %v", r.GetKind())
}
Expand Down
Loading