diff --git a/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go index 9265624585c97..3cb6b645d09ac 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go @@ -4252,6 +4252,107 @@ func (x *ConnectToDesktopResponse) GetData() []byte { return nil } +// Request for AttachDirectoryToDesktopSession. +type AttachDirectoryToDesktopSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // URI of the desktop. + DesktopUri string `protobuf:"bytes,1,opt,name=desktop_uri,json=desktopUri,proto3" json:"desktop_uri,omitempty"` + // Login for the desktop session. + Login string `protobuf:"bytes,2,opt,name=login,proto3" json:"login,omitempty"` + // Path to share with a remote machine. Must be a directory. + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttachDirectoryToDesktopSessionRequest) Reset() { + *x = AttachDirectoryToDesktopSessionRequest{} + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[75] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttachDirectoryToDesktopSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttachDirectoryToDesktopSessionRequest) ProtoMessage() {} + +func (x *AttachDirectoryToDesktopSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[75] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttachDirectoryToDesktopSessionRequest.ProtoReflect.Descriptor instead. +func (*AttachDirectoryToDesktopSessionRequest) Descriptor() ([]byte, []int) { + return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{75} +} + +func (x *AttachDirectoryToDesktopSessionRequest) GetDesktopUri() string { + if x != nil { + return x.DesktopUri + } + return "" +} + +func (x *AttachDirectoryToDesktopSessionRequest) GetLogin() string { + if x != nil { + return x.Login + } + return "" +} + +func (x *AttachDirectoryToDesktopSessionRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +// Response for AttachDirectoryToDesktopSession. +type AttachDirectoryToDesktopSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttachDirectoryToDesktopSessionResponse) Reset() { + *x = AttachDirectoryToDesktopSessionResponse{} + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[76] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttachDirectoryToDesktopSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttachDirectoryToDesktopSessionResponse) ProtoMessage() {} + +func (x *AttachDirectoryToDesktopSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[76] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttachDirectoryToDesktopSessionResponse.ProtoReflect.Descriptor instead. +func (*AttachDirectoryToDesktopSessionResponse) Descriptor() ([]byte, []int) { + return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{76} +} + // LoginPasswordlessRequestInit contains fields needed to init the stream request. type LoginPasswordlessRequest_LoginPasswordlessRequestInit struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4263,7 +4364,7 @@ type LoginPasswordlessRequest_LoginPasswordlessRequestInit struct { func (x *LoginPasswordlessRequest_LoginPasswordlessRequestInit) Reset() { *x = LoginPasswordlessRequest_LoginPasswordlessRequestInit{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[75] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4275,7 +4376,7 @@ func (x *LoginPasswordlessRequest_LoginPasswordlessRequestInit) String() string func (*LoginPasswordlessRequest_LoginPasswordlessRequestInit) ProtoMessage() {} func (x *LoginPasswordlessRequest_LoginPasswordlessRequestInit) ProtoReflect() protoreflect.Message { - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[75] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4309,7 +4410,7 @@ type LoginPasswordlessRequest_LoginPasswordlessPINResponse struct { func (x *LoginPasswordlessRequest_LoginPasswordlessPINResponse) Reset() { *x = LoginPasswordlessRequest_LoginPasswordlessPINResponse{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[76] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4321,7 +4422,7 @@ func (x *LoginPasswordlessRequest_LoginPasswordlessPINResponse) String() string func (*LoginPasswordlessRequest_LoginPasswordlessPINResponse) ProtoMessage() {} func (x *LoginPasswordlessRequest_LoginPasswordlessPINResponse) ProtoReflect() protoreflect.Message { - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[76] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4357,7 +4458,7 @@ type LoginPasswordlessRequest_LoginPasswordlessCredentialResponse struct { func (x *LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) Reset() { *x = LoginPasswordlessRequest_LoginPasswordlessCredentialResponse{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[77] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4369,7 +4470,7 @@ func (x *LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) String() func (*LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) ProtoMessage() {} func (x *LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) ProtoReflect() protoreflect.Message { - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[77] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4407,7 +4508,7 @@ type LoginRequest_LocalParams struct { func (x *LoginRequest_LocalParams) Reset() { *x = LoginRequest_LocalParams{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[78] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4419,7 +4520,7 @@ func (x *LoginRequest_LocalParams) String() string { func (*LoginRequest_LocalParams) ProtoMessage() {} func (x *LoginRequest_LocalParams) ProtoReflect() protoreflect.Message { - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[78] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4469,7 +4570,7 @@ type LoginRequest_SsoParams struct { func (x *LoginRequest_SsoParams) Reset() { *x = LoginRequest_SsoParams{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[79] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4481,7 +4582,7 @@ func (x *LoginRequest_SsoParams) String() string { func (*LoginRequest_SsoParams) ProtoMessage() {} func (x *LoginRequest_SsoParams) ProtoReflect() protoreflect.Message { - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[79] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4783,7 +4884,13 @@ const file_teleport_lib_teleterm_v1_service_proto_rawDesc = "" + "\x04data\x18\x01 \x01(\fR\x04data\x12N\n" + "\x0etarget_desktop\x18\x02 \x01(\v2'.teleport.lib.teleterm.v1.TargetDesktopR\rtargetDesktop\".\n" + "\x18ConnectToDesktopResponse\x12\x12\n" + - "\x04data\x18\x01 \x01(\fR\x04data*\x97\x01\n" + + "\x04data\x18\x01 \x01(\fR\x04data\"s\n" + + "&AttachDirectoryToDesktopSessionRequest\x12\x1f\n" + + "\vdesktop_uri\x18\x01 \x01(\tR\n" + + "desktopUri\x12\x14\n" + + "\x05login\x18\x02 \x01(\tR\x05login\x12\x12\n" + + "\x04path\x18\x03 \x01(\tR\x04path\")\n" + + "'AttachDirectoryToDesktopSessionResponse*\x97\x01\n" + "\x12PasswordlessPrompt\x12#\n" + "\x1fPASSWORDLESS_PROMPT_UNSPECIFIED\x10\x00\x12\x1b\n" + "\x17PASSWORDLESS_PROMPT_PIN\x10\x01\x12\x1b\n" + @@ -4797,7 +4904,7 @@ const file_teleport_lib_teleterm_v1_service_proto_rawDesc = "" + ")HEADLESS_AUTHENTICATION_STATE_UNSPECIFIED\x10\x00\x12)\n" + "%HEADLESS_AUTHENTICATION_STATE_PENDING\x10\x01\x12(\n" + "$HEADLESS_AUTHENTICATION_STATE_DENIED\x10\x02\x12*\n" + - "&HEADLESS_AUTHENTICATION_STATE_APPROVED\x10\x032\xdd)\n" + + "&HEADLESS_AUTHENTICATION_STATE_APPROVED\x10\x032\x86+\n" + "\x0fTerminalService\x12\xa0\x01\n" + "\x1dUpdateTshdEventsServerAddress\x12>.teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressRequest\x1a?.teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressResponse\x12q\n" + "\x10ListRootClusters\x12-.teleport.lib.teleterm.v1.ListClustersRequest\x1a..teleport.lib.teleterm.v1.ListClustersResponse\x12u\n" + @@ -4844,7 +4951,8 @@ const file_teleport_lib_teleterm_v1_service_proto_rawDesc = "" + "\x15UpdateUserPreferences\x126.teleport.lib.teleterm.v1.UpdateUserPreferencesRequest\x1a7.teleport.lib.teleterm.v1.UpdateUserPreferencesResponse\x12\x88\x01\n" + "\x15AuthenticateWebDevice\x126.teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest\x1a7.teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse\x12[\n" + "\x06GetApp\x12'.teleport.lib.teleterm.v1.GetAppRequest\x1a(.teleport.lib.teleterm.v1.GetAppResponse\x12}\n" + - "\x10ConnectToDesktop\x121.teleport.lib.teleterm.v1.ConnectToDesktopRequest\x1a2.teleport.lib.teleterm.v1.ConnectToDesktopResponse(\x010\x01BTZRgithub.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1;teletermv1b\x06proto3" + "\x10ConnectToDesktop\x121.teleport.lib.teleterm.v1.ConnectToDesktopRequest\x1a2.teleport.lib.teleterm.v1.ConnectToDesktopResponse(\x010\x01\x12\xa6\x01\n" + + "\x1fAttachDirectoryToDesktopSession\x12@.teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionRequest\x1aA.teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionResponseBTZRgithub.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1;teletermv1b\x06proto3" var ( file_teleport_lib_teleterm_v1_service_proto_rawDescOnce sync.Once @@ -4859,7 +4967,7 @@ func file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP() []byte { } var file_teleport_lib_teleterm_v1_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_teleport_lib_teleterm_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 80) +var file_teleport_lib_teleterm_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 82) var file_teleport_lib_teleterm_v1_service_proto_goTypes = []any{ (PasswordlessPrompt)(0), // 0: teleport.lib.teleterm.v1.PasswordlessPrompt (FileTransferDirection)(0), // 1: teleport.lib.teleterm.v1.FileTransferDirection @@ -4939,72 +5047,74 @@ var file_teleport_lib_teleterm_v1_service_proto_goTypes = []any{ (*TargetDesktop)(nil), // 75: teleport.lib.teleterm.v1.TargetDesktop (*ConnectToDesktopRequest)(nil), // 76: teleport.lib.teleterm.v1.ConnectToDesktopRequest (*ConnectToDesktopResponse)(nil), // 77: teleport.lib.teleterm.v1.ConnectToDesktopResponse - (*LoginPasswordlessRequest_LoginPasswordlessRequestInit)(nil), // 78: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit - (*LoginPasswordlessRequest_LoginPasswordlessPINResponse)(nil), // 79: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse - (*LoginPasswordlessRequest_LoginPasswordlessCredentialResponse)(nil), // 80: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse - (*LoginRequest_LocalParams)(nil), // 81: teleport.lib.teleterm.v1.LoginRequest.LocalParams - (*LoginRequest_SsoParams)(nil), // 82: teleport.lib.teleterm.v1.LoginRequest.SsoParams - (*AccessRequest)(nil), // 83: teleport.lib.teleterm.v1.AccessRequest - (*ResourceID)(nil), // 84: teleport.lib.teleterm.v1.ResourceID - (*timestamppb.Timestamp)(nil), // 85: google.protobuf.Timestamp - (*v1.AccessList)(nil), // 86: teleport.accesslist.v1.AccessList - (*KubeResource)(nil), // 87: teleport.lib.teleterm.v1.KubeResource - (*Cluster)(nil), // 88: teleport.lib.teleterm.v1.Cluster - (*Gateway)(nil), // 89: teleport.lib.teleterm.v1.Gateway - (*Server)(nil), // 90: teleport.lib.teleterm.v1.Server - (*Database)(nil), // 91: teleport.lib.teleterm.v1.Database - (*Kube)(nil), // 92: teleport.lib.teleterm.v1.Kube - (*App)(nil), // 93: teleport.lib.teleterm.v1.App - (*WindowsDesktop)(nil), // 94: teleport.lib.teleterm.v1.WindowsDesktop - (*v11.ClusterUserPreferences)(nil), // 95: teleport.userpreferences.v1.ClusterUserPreferences - (*v11.UnifiedResourcePreferences)(nil), // 96: teleport.userpreferences.v1.UnifiedResourcePreferences - (*v12.DeviceWebToken)(nil), // 97: teleport.devicetrust.v1.DeviceWebToken - (*v12.DeviceConfirmationToken)(nil), // 98: teleport.devicetrust.v1.DeviceConfirmationToken - (*ReportUsageEventRequest)(nil), // 99: teleport.lib.teleterm.v1.ReportUsageEventRequest - (*AuthSettings)(nil), // 100: teleport.lib.teleterm.v1.AuthSettings + (*AttachDirectoryToDesktopSessionRequest)(nil), // 78: teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionRequest + (*AttachDirectoryToDesktopSessionResponse)(nil), // 79: teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionResponse + (*LoginPasswordlessRequest_LoginPasswordlessRequestInit)(nil), // 80: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit + (*LoginPasswordlessRequest_LoginPasswordlessPINResponse)(nil), // 81: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse + (*LoginPasswordlessRequest_LoginPasswordlessCredentialResponse)(nil), // 82: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse + (*LoginRequest_LocalParams)(nil), // 83: teleport.lib.teleterm.v1.LoginRequest.LocalParams + (*LoginRequest_SsoParams)(nil), // 84: teleport.lib.teleterm.v1.LoginRequest.SsoParams + (*AccessRequest)(nil), // 85: teleport.lib.teleterm.v1.AccessRequest + (*ResourceID)(nil), // 86: teleport.lib.teleterm.v1.ResourceID + (*timestamppb.Timestamp)(nil), // 87: google.protobuf.Timestamp + (*v1.AccessList)(nil), // 88: teleport.accesslist.v1.AccessList + (*KubeResource)(nil), // 89: teleport.lib.teleterm.v1.KubeResource + (*Cluster)(nil), // 90: teleport.lib.teleterm.v1.Cluster + (*Gateway)(nil), // 91: teleport.lib.teleterm.v1.Gateway + (*Server)(nil), // 92: teleport.lib.teleterm.v1.Server + (*Database)(nil), // 93: teleport.lib.teleterm.v1.Database + (*Kube)(nil), // 94: teleport.lib.teleterm.v1.Kube + (*App)(nil), // 95: teleport.lib.teleterm.v1.App + (*WindowsDesktop)(nil), // 96: teleport.lib.teleterm.v1.WindowsDesktop + (*v11.ClusterUserPreferences)(nil), // 97: teleport.userpreferences.v1.ClusterUserPreferences + (*v11.UnifiedResourcePreferences)(nil), // 98: teleport.userpreferences.v1.UnifiedResourcePreferences + (*v12.DeviceWebToken)(nil), // 99: teleport.devicetrust.v1.DeviceWebToken + (*v12.DeviceConfirmationToken)(nil), // 100: teleport.devicetrust.v1.DeviceConfirmationToken + (*ReportUsageEventRequest)(nil), // 101: teleport.lib.teleterm.v1.ReportUsageEventRequest + (*AuthSettings)(nil), // 102: teleport.lib.teleterm.v1.AuthSettings } var file_teleport_lib_teleterm_v1_service_proto_depIdxs = []int32{ - 83, // 0: teleport.lib.teleterm.v1.GetAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest - 83, // 1: teleport.lib.teleterm.v1.GetAccessRequestsResponse.requests:type_name -> teleport.lib.teleterm.v1.AccessRequest - 84, // 2: teleport.lib.teleterm.v1.CreateAccessRequestRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID - 85, // 3: teleport.lib.teleterm.v1.CreateAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp - 85, // 4: teleport.lib.teleterm.v1.CreateAccessRequestRequest.max_duration:type_name -> google.protobuf.Timestamp - 85, // 5: teleport.lib.teleterm.v1.CreateAccessRequestRequest.request_ttl:type_name -> google.protobuf.Timestamp - 83, // 6: teleport.lib.teleterm.v1.CreateAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest - 84, // 7: teleport.lib.teleterm.v1.GetRequestableRolesRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID - 85, // 8: teleport.lib.teleterm.v1.ReviewAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp - 83, // 9: teleport.lib.teleterm.v1.ReviewAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest - 83, // 10: teleport.lib.teleterm.v1.PromoteAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest - 86, // 11: teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse.access_lists:type_name -> teleport.accesslist.v1.AccessList - 87, // 12: teleport.lib.teleterm.v1.ListKubernetesResourcesResponse.resources:type_name -> teleport.lib.teleterm.v1.KubeResource + 85, // 0: teleport.lib.teleterm.v1.GetAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest + 85, // 1: teleport.lib.teleterm.v1.GetAccessRequestsResponse.requests:type_name -> teleport.lib.teleterm.v1.AccessRequest + 86, // 2: teleport.lib.teleterm.v1.CreateAccessRequestRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID + 87, // 3: teleport.lib.teleterm.v1.CreateAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp + 87, // 4: teleport.lib.teleterm.v1.CreateAccessRequestRequest.max_duration:type_name -> google.protobuf.Timestamp + 87, // 5: teleport.lib.teleterm.v1.CreateAccessRequestRequest.request_ttl:type_name -> google.protobuf.Timestamp + 85, // 6: teleport.lib.teleterm.v1.CreateAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest + 86, // 7: teleport.lib.teleterm.v1.GetRequestableRolesRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID + 87, // 8: teleport.lib.teleterm.v1.ReviewAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp + 85, // 9: teleport.lib.teleterm.v1.ReviewAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest + 85, // 10: teleport.lib.teleterm.v1.PromoteAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest + 88, // 11: teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse.access_lists:type_name -> teleport.accesslist.v1.AccessList + 89, // 12: teleport.lib.teleterm.v1.ListKubernetesResourcesResponse.resources:type_name -> teleport.lib.teleterm.v1.KubeResource 0, // 13: teleport.lib.teleterm.v1.LoginPasswordlessResponse.prompt:type_name -> teleport.lib.teleterm.v1.PasswordlessPrompt 27, // 14: teleport.lib.teleterm.v1.LoginPasswordlessResponse.credentials:type_name -> teleport.lib.teleterm.v1.CredentialInfo - 78, // 15: teleport.lib.teleterm.v1.LoginPasswordlessRequest.init:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit - 79, // 16: teleport.lib.teleterm.v1.LoginPasswordlessRequest.pin:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse - 80, // 17: teleport.lib.teleterm.v1.LoginPasswordlessRequest.credential:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse + 80, // 15: teleport.lib.teleterm.v1.LoginPasswordlessRequest.init:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit + 81, // 16: teleport.lib.teleterm.v1.LoginPasswordlessRequest.pin:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse + 82, // 17: teleport.lib.teleterm.v1.LoginPasswordlessRequest.credential:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse 1, // 18: teleport.lib.teleterm.v1.FileTransferRequest.direction:type_name -> teleport.lib.teleterm.v1.FileTransferDirection - 81, // 19: teleport.lib.teleterm.v1.LoginRequest.local:type_name -> teleport.lib.teleterm.v1.LoginRequest.LocalParams - 82, // 20: teleport.lib.teleterm.v1.LoginRequest.sso:type_name -> teleport.lib.teleterm.v1.LoginRequest.SsoParams - 88, // 21: teleport.lib.teleterm.v1.ListClustersResponse.clusters:type_name -> teleport.lib.teleterm.v1.Cluster - 89, // 22: teleport.lib.teleterm.v1.ListGatewaysResponse.gateways:type_name -> teleport.lib.teleterm.v1.Gateway - 90, // 23: teleport.lib.teleterm.v1.GetServersResponse.agents:type_name -> teleport.lib.teleterm.v1.Server + 83, // 19: teleport.lib.teleterm.v1.LoginRequest.local:type_name -> teleport.lib.teleterm.v1.LoginRequest.LocalParams + 84, // 20: teleport.lib.teleterm.v1.LoginRequest.sso:type_name -> teleport.lib.teleterm.v1.LoginRequest.SsoParams + 90, // 21: teleport.lib.teleterm.v1.ListClustersResponse.clusters:type_name -> teleport.lib.teleterm.v1.Cluster + 91, // 22: teleport.lib.teleterm.v1.ListGatewaysResponse.gateways:type_name -> teleport.lib.teleterm.v1.Gateway + 92, // 23: teleport.lib.teleterm.v1.GetServersResponse.agents:type_name -> teleport.lib.teleterm.v1.Server 2, // 24: teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest.state:type_name -> teleport.lib.teleterm.v1.HeadlessAuthenticationState - 90, // 25: teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse.server:type_name -> teleport.lib.teleterm.v1.Server + 92, // 25: teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse.server:type_name -> teleport.lib.teleterm.v1.Server 63, // 26: teleport.lib.teleterm.v1.ListUnifiedResourcesRequest.sort_by:type_name -> teleport.lib.teleterm.v1.SortBy 65, // 27: teleport.lib.teleterm.v1.ListUnifiedResourcesResponse.resources:type_name -> teleport.lib.teleterm.v1.PaginatedResource - 91, // 28: teleport.lib.teleterm.v1.PaginatedResource.database:type_name -> teleport.lib.teleterm.v1.Database - 90, // 29: teleport.lib.teleterm.v1.PaginatedResource.server:type_name -> teleport.lib.teleterm.v1.Server - 92, // 30: teleport.lib.teleterm.v1.PaginatedResource.kube:type_name -> teleport.lib.teleterm.v1.Kube - 93, // 31: teleport.lib.teleterm.v1.PaginatedResource.app:type_name -> teleport.lib.teleterm.v1.App - 94, // 32: teleport.lib.teleterm.v1.PaginatedResource.windows_desktop:type_name -> teleport.lib.teleterm.v1.WindowsDesktop + 93, // 28: teleport.lib.teleterm.v1.PaginatedResource.database:type_name -> teleport.lib.teleterm.v1.Database + 92, // 29: teleport.lib.teleterm.v1.PaginatedResource.server:type_name -> teleport.lib.teleterm.v1.Server + 94, // 30: teleport.lib.teleterm.v1.PaginatedResource.kube:type_name -> teleport.lib.teleterm.v1.Kube + 95, // 31: teleport.lib.teleterm.v1.PaginatedResource.app:type_name -> teleport.lib.teleterm.v1.App + 96, // 32: teleport.lib.teleterm.v1.PaginatedResource.windows_desktop:type_name -> teleport.lib.teleterm.v1.WindowsDesktop 70, // 33: teleport.lib.teleterm.v1.GetUserPreferencesResponse.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences 70, // 34: teleport.lib.teleterm.v1.UpdateUserPreferencesRequest.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences 70, // 35: teleport.lib.teleterm.v1.UpdateUserPreferencesResponse.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences - 95, // 36: teleport.lib.teleterm.v1.UserPreferences.cluster_preferences:type_name -> teleport.userpreferences.v1.ClusterUserPreferences - 96, // 37: teleport.lib.teleterm.v1.UserPreferences.unified_resource_preferences:type_name -> teleport.userpreferences.v1.UnifiedResourcePreferences - 97, // 38: teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest.device_web_token:type_name -> teleport.devicetrust.v1.DeviceWebToken - 98, // 39: teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse.confirmation_token:type_name -> teleport.devicetrust.v1.DeviceConfirmationToken - 93, // 40: teleport.lib.teleterm.v1.GetAppResponse.app:type_name -> teleport.lib.teleterm.v1.App + 97, // 36: teleport.lib.teleterm.v1.UserPreferences.cluster_preferences:type_name -> teleport.userpreferences.v1.ClusterUserPreferences + 98, // 37: teleport.lib.teleterm.v1.UserPreferences.unified_resource_preferences:type_name -> teleport.userpreferences.v1.UnifiedResourcePreferences + 99, // 38: teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest.device_web_token:type_name -> teleport.devicetrust.v1.DeviceWebToken + 100, // 39: teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse.confirmation_token:type_name -> teleport.devicetrust.v1.DeviceConfirmationToken + 95, // 40: teleport.lib.teleterm.v1.GetAppResponse.app:type_name -> teleport.lib.teleterm.v1.App 75, // 41: teleport.lib.teleterm.v1.ConnectToDesktopRequest.target_desktop:type_name -> teleport.lib.teleterm.v1.TargetDesktop 48, // 42: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:input_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressRequest 34, // 43: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:input_type -> teleport.lib.teleterm.v1.ListClustersRequest @@ -5035,7 +5145,7 @@ var file_teleport_lib_teleterm_v1_service_proto_depIdxs = []int32{ 29, // 68: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:input_type -> teleport.lib.teleterm.v1.LoginPasswordlessRequest 6, // 69: teleport.lib.teleterm.v1.TerminalService.Logout:input_type -> teleport.lib.teleterm.v1.LogoutRequest 30, // 70: teleport.lib.teleterm.v1.TerminalService.TransferFile:input_type -> teleport.lib.teleterm.v1.FileTransferRequest - 99, // 71: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:input_type -> teleport.lib.teleterm.v1.ReportUsageEventRequest + 101, // 71: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:input_type -> teleport.lib.teleterm.v1.ReportUsageEventRequest 50, // 72: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:input_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest 52, // 73: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleRequest 54, // 74: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenRequest @@ -5048,50 +5158,52 @@ var file_teleport_lib_teleterm_v1_service_proto_depIdxs = []int32{ 71, // 81: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:input_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest 73, // 82: teleport.lib.teleterm.v1.TerminalService.GetApp:input_type -> teleport.lib.teleterm.v1.GetAppRequest 76, // 83: teleport.lib.teleterm.v1.TerminalService.ConnectToDesktop:input_type -> teleport.lib.teleterm.v1.ConnectToDesktopRequest - 49, // 84: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:output_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressResponse - 35, // 85: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse - 35, // 86: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse - 8, // 87: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:output_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherResponse - 38, // 88: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:output_type -> teleport.lib.teleterm.v1.ListDatabaseUsersResponse - 46, // 89: teleport.lib.teleterm.v1.TerminalService.GetServers:output_type -> teleport.lib.teleterm.v1.GetServersResponse - 12, // 90: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:output_type -> teleport.lib.teleterm.v1.GetAccessRequestsResponse - 11, // 91: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:output_type -> teleport.lib.teleterm.v1.GetAccessRequestResponse - 3, // 92: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 15, // 93: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:output_type -> teleport.lib.teleterm.v1.CreateAccessRequestResponse - 20, // 94: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:output_type -> teleport.lib.teleterm.v1.ReviewAccessRequestResponse - 18, // 95: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:output_type -> teleport.lib.teleterm.v1.GetRequestableRolesResponse - 3, // 96: teleport.lib.teleterm.v1.TerminalService.AssumeRole:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 22, // 97: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:output_type -> teleport.lib.teleterm.v1.PromoteAccessRequestResponse - 24, // 98: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:output_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse - 26, // 99: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:output_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesResponse - 88, // 100: teleport.lib.teleterm.v1.TerminalService.AddCluster:output_type -> teleport.lib.teleterm.v1.Cluster - 3, // 101: teleport.lib.teleterm.v1.TerminalService.RemoveCluster:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 41, // 102: teleport.lib.teleterm.v1.TerminalService.ListGateways:output_type -> teleport.lib.teleterm.v1.ListGatewaysResponse - 89, // 103: teleport.lib.teleterm.v1.TerminalService.CreateGateway:output_type -> teleport.lib.teleterm.v1.Gateway - 3, // 104: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 89, // 105: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:output_type -> teleport.lib.teleterm.v1.Gateway - 89, // 106: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:output_type -> teleport.lib.teleterm.v1.Gateway - 100, // 107: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:output_type -> teleport.lib.teleterm.v1.AuthSettings - 88, // 108: teleport.lib.teleterm.v1.TerminalService.GetCluster:output_type -> teleport.lib.teleterm.v1.Cluster - 3, // 109: teleport.lib.teleterm.v1.TerminalService.Login:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 28, // 110: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:output_type -> teleport.lib.teleterm.v1.LoginPasswordlessResponse - 3, // 111: teleport.lib.teleterm.v1.TerminalService.Logout:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 31, // 112: teleport.lib.teleterm.v1.TerminalService.TransferFile:output_type -> teleport.lib.teleterm.v1.FileTransferProgress - 3, // 113: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 51, // 114: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:output_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateResponse - 53, // 115: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleResponse - 55, // 116: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenResponse - 57, // 117: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:output_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse - 59, // 118: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:output_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeResponse - 61, // 119: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:output_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameResponse - 64, // 120: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:output_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesResponse - 67, // 121: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:output_type -> teleport.lib.teleterm.v1.GetUserPreferencesResponse - 69, // 122: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:output_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesResponse - 72, // 123: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:output_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse - 74, // 124: teleport.lib.teleterm.v1.TerminalService.GetApp:output_type -> teleport.lib.teleterm.v1.GetAppResponse - 77, // 125: teleport.lib.teleterm.v1.TerminalService.ConnectToDesktop:output_type -> teleport.lib.teleterm.v1.ConnectToDesktopResponse - 84, // [84:126] is the sub-list for method output_type - 42, // [42:84] is the sub-list for method input_type + 78, // 84: teleport.lib.teleterm.v1.TerminalService.AttachDirectoryToDesktopSession:input_type -> teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionRequest + 49, // 85: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:output_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressResponse + 35, // 86: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse + 35, // 87: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse + 8, // 88: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:output_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherResponse + 38, // 89: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:output_type -> teleport.lib.teleterm.v1.ListDatabaseUsersResponse + 46, // 90: teleport.lib.teleterm.v1.TerminalService.GetServers:output_type -> teleport.lib.teleterm.v1.GetServersResponse + 12, // 91: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:output_type -> teleport.lib.teleterm.v1.GetAccessRequestsResponse + 11, // 92: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:output_type -> teleport.lib.teleterm.v1.GetAccessRequestResponse + 3, // 93: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 15, // 94: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:output_type -> teleport.lib.teleterm.v1.CreateAccessRequestResponse + 20, // 95: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:output_type -> teleport.lib.teleterm.v1.ReviewAccessRequestResponse + 18, // 96: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:output_type -> teleport.lib.teleterm.v1.GetRequestableRolesResponse + 3, // 97: teleport.lib.teleterm.v1.TerminalService.AssumeRole:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 22, // 98: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:output_type -> teleport.lib.teleterm.v1.PromoteAccessRequestResponse + 24, // 99: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:output_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse + 26, // 100: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:output_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesResponse + 90, // 101: teleport.lib.teleterm.v1.TerminalService.AddCluster:output_type -> teleport.lib.teleterm.v1.Cluster + 3, // 102: teleport.lib.teleterm.v1.TerminalService.RemoveCluster:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 41, // 103: teleport.lib.teleterm.v1.TerminalService.ListGateways:output_type -> teleport.lib.teleterm.v1.ListGatewaysResponse + 91, // 104: teleport.lib.teleterm.v1.TerminalService.CreateGateway:output_type -> teleport.lib.teleterm.v1.Gateway + 3, // 105: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 91, // 106: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:output_type -> teleport.lib.teleterm.v1.Gateway + 91, // 107: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:output_type -> teleport.lib.teleterm.v1.Gateway + 102, // 108: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:output_type -> teleport.lib.teleterm.v1.AuthSettings + 90, // 109: teleport.lib.teleterm.v1.TerminalService.GetCluster:output_type -> teleport.lib.teleterm.v1.Cluster + 3, // 110: teleport.lib.teleterm.v1.TerminalService.Login:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 28, // 111: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:output_type -> teleport.lib.teleterm.v1.LoginPasswordlessResponse + 3, // 112: teleport.lib.teleterm.v1.TerminalService.Logout:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 31, // 113: teleport.lib.teleterm.v1.TerminalService.TransferFile:output_type -> teleport.lib.teleterm.v1.FileTransferProgress + 3, // 114: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 51, // 115: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:output_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateResponse + 53, // 116: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleResponse + 55, // 117: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenResponse + 57, // 118: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:output_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse + 59, // 119: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:output_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeResponse + 61, // 120: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:output_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameResponse + 64, // 121: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:output_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesResponse + 67, // 122: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:output_type -> teleport.lib.teleterm.v1.GetUserPreferencesResponse + 69, // 123: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:output_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesResponse + 72, // 124: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:output_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse + 74, // 125: teleport.lib.teleterm.v1.TerminalService.GetApp:output_type -> teleport.lib.teleterm.v1.GetAppResponse + 77, // 126: teleport.lib.teleterm.v1.TerminalService.ConnectToDesktop:output_type -> teleport.lib.teleterm.v1.ConnectToDesktopResponse + 79, // 127: teleport.lib.teleterm.v1.TerminalService.AttachDirectoryToDesktopSession:output_type -> teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionResponse + 85, // [85:128] is the sub-list for method output_type + 42, // [42:85] is the sub-list for method input_type 42, // [42:42] is the sub-list for extension type_name 42, // [42:42] is the sub-list for extension extendee 0, // [0:42] is the sub-list for field type_name @@ -5134,7 +5246,7 @@ func file_teleport_lib_teleterm_v1_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_teleport_lib_teleterm_v1_service_proto_rawDesc), len(file_teleport_lib_teleterm_v1_service_proto_rawDesc)), NumEnums: 3, - NumMessages: 80, + NumMessages: 82, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/proto/go/teleport/lib/teleterm/v1/service_grpc.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/service_grpc.pb.go index eacaf31589bb2..2b10f6293289b 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/service_grpc.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/service_grpc.pb.go @@ -78,6 +78,7 @@ const ( TerminalService_AuthenticateWebDevice_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/AuthenticateWebDevice" TerminalService_GetApp_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/GetApp" TerminalService_ConnectToDesktop_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/ConnectToDesktop" + TerminalService_AttachDirectoryToDesktopSession_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/AttachDirectoryToDesktopSession" ) // TerminalServiceClient is the client API for TerminalService service. @@ -220,6 +221,13 @@ type TerminalServiceClient interface { GetApp(ctx context.Context, in *GetAppRequest, opts ...grpc.CallOption) (*GetAppResponse, error) // ConnectToDesktop is a bidirectional stream for the desktop connection. ConnectToDesktop(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ConnectToDesktopRequest, ConnectToDesktopResponse], error) + // AttachDirectoryToDesktopSession opens a directory for a desktop session and enables file system operations for it. + // If there is no active desktop session associated with the specified desktop_uri and login, + // the RPC returns an error. + // + // This RPC does not automatically share the directory with the server (it does not send a SharedDirectoryAnnounce message). + // It only registers file system handlers for processing file system-related TDP events. + AttachDirectoryToDesktopSession(ctx context.Context, in *AttachDirectoryToDesktopSessionRequest, opts ...grpc.CallOption) (*AttachDirectoryToDesktopSessionResponse, error) } type terminalServiceClient struct { @@ -666,6 +674,16 @@ func (c *terminalServiceClient) ConnectToDesktop(ctx context.Context, opts ...gr // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type TerminalService_ConnectToDesktopClient = grpc.BidiStreamingClient[ConnectToDesktopRequest, ConnectToDesktopResponse] +func (c *terminalServiceClient) AttachDirectoryToDesktopSession(ctx context.Context, in *AttachDirectoryToDesktopSessionRequest, opts ...grpc.CallOption) (*AttachDirectoryToDesktopSessionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AttachDirectoryToDesktopSessionResponse) + err := c.cc.Invoke(ctx, TerminalService_AttachDirectoryToDesktopSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // TerminalServiceServer is the server API for TerminalService service. // All implementations must embed UnimplementedTerminalServiceServer // for forward compatibility. @@ -806,6 +824,13 @@ type TerminalServiceServer interface { GetApp(context.Context, *GetAppRequest) (*GetAppResponse, error) // ConnectToDesktop is a bidirectional stream for the desktop connection. ConnectToDesktop(grpc.BidiStreamingServer[ConnectToDesktopRequest, ConnectToDesktopResponse]) error + // AttachDirectoryToDesktopSession opens a directory for a desktop session and enables file system operations for it. + // If there is no active desktop session associated with the specified desktop_uri and login, + // the RPC returns an error. + // + // This RPC does not automatically share the directory with the server (it does not send a SharedDirectoryAnnounce message). + // It only registers file system handlers for processing file system-related TDP events. + AttachDirectoryToDesktopSession(context.Context, *AttachDirectoryToDesktopSessionRequest) (*AttachDirectoryToDesktopSessionResponse, error) mustEmbedUnimplementedTerminalServiceServer() } @@ -942,6 +967,9 @@ func (UnimplementedTerminalServiceServer) GetApp(context.Context, *GetAppRequest func (UnimplementedTerminalServiceServer) ConnectToDesktop(grpc.BidiStreamingServer[ConnectToDesktopRequest, ConnectToDesktopResponse]) error { return status.Errorf(codes.Unimplemented, "method ConnectToDesktop not implemented") } +func (UnimplementedTerminalServiceServer) AttachDirectoryToDesktopSession(context.Context, *AttachDirectoryToDesktopSessionRequest) (*AttachDirectoryToDesktopSessionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AttachDirectoryToDesktopSession not implemented") +} func (UnimplementedTerminalServiceServer) mustEmbedUnimplementedTerminalServiceServer() {} func (UnimplementedTerminalServiceServer) testEmbeddedByValue() {} @@ -1690,6 +1718,24 @@ func _TerminalService_ConnectToDesktop_Handler(srv interface{}, stream grpc.Serv // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type TerminalService_ConnectToDesktopServer = grpc.BidiStreamingServer[ConnectToDesktopRequest, ConnectToDesktopResponse] +func _TerminalService_AttachDirectoryToDesktopSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AttachDirectoryToDesktopSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TerminalServiceServer).AttachDirectoryToDesktopSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TerminalService_AttachDirectoryToDesktopSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TerminalServiceServer).AttachDirectoryToDesktopSession(ctx, req.(*AttachDirectoryToDesktopSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + // TerminalService_ServiceDesc is the grpc.ServiceDesc for TerminalService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1853,6 +1899,10 @@ var TerminalService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetApp", Handler: _TerminalService_GetApp_Handler, }, + { + MethodName: "AttachDirectoryToDesktopSession", + Handler: _TerminalService_AttachDirectoryToDesktopSession_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.client.ts b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.client.ts index d53cd99d91522..7165cf77fef08 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.client.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.client.ts @@ -24,6 +24,8 @@ import type { RpcTransport } from "@protobuf-ts/runtime-rpc"; import type { ServiceInfo } from "@protobuf-ts/runtime-rpc"; import { TerminalService } from "./service_pb"; +import type { AttachDirectoryToDesktopSessionResponse } from "./service_pb"; +import type { AttachDirectoryToDesktopSessionRequest } from "./service_pb"; import type { ConnectToDesktopResponse } from "./service_pb"; import type { ConnectToDesktopRequest } from "./service_pb"; import type { GetAppResponse } from "./service_pb"; @@ -411,6 +413,17 @@ export interface ITerminalServiceClient { * @generated from protobuf rpc: ConnectToDesktop(stream teleport.lib.teleterm.v1.ConnectToDesktopRequest) returns (stream teleport.lib.teleterm.v1.ConnectToDesktopResponse); */ connectToDesktop(options?: RpcOptions): DuplexStreamingCall; + /** + * AttachDirectoryToDesktopSession opens a directory for a desktop session and enables file system operations for it. + * If there is no active desktop session associated with the specified desktop_uri and login, + * the RPC returns an error. + * + * This RPC does not automatically share the directory with the server (it does not send a SharedDirectoryAnnounce message). + * It only registers file system handlers for processing file system-related TDP events. + * + * @generated from protobuf rpc: AttachDirectoryToDesktopSession(teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionRequest) returns (teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionResponse); + */ + attachDirectoryToDesktopSession(input: AttachDirectoryToDesktopSessionRequest, options?: RpcOptions): UnaryCall; } /** * TerminalService is used by the Electron app to communicate with the tsh daemon. @@ -851,4 +864,18 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf const method = this.methods[41], opt = this._transport.mergeOptions(options); return stackIntercept("duplex", this._transport, method, opt); } + /** + * AttachDirectoryToDesktopSession opens a directory for a desktop session and enables file system operations for it. + * If there is no active desktop session associated with the specified desktop_uri and login, + * the RPC returns an error. + * + * This RPC does not automatically share the directory with the server (it does not send a SharedDirectoryAnnounce message). + * It only registers file system handlers for processing file system-related TDP events. + * + * @generated from protobuf rpc: AttachDirectoryToDesktopSession(teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionRequest) returns (teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionResponse); + */ + attachDirectoryToDesktopSession(input: AttachDirectoryToDesktopSessionRequest, options?: RpcOptions): UnaryCall { + const method = this.methods[42], opt = this._transport.mergeOptions(options); + return stackIntercept("unary", this._transport, method, opt, input); + } } diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.grpc-server.ts b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.grpc-server.ts index 533a756170162..3f2fca6c24949 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.grpc-server.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.grpc-server.ts @@ -21,6 +21,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // +import { AttachDirectoryToDesktopSessionResponse } from "./service_pb"; +import { AttachDirectoryToDesktopSessionRequest } from "./service_pb"; import { ConnectToDesktopResponse } from "./service_pb"; import { ConnectToDesktopRequest } from "./service_pb"; import { GetAppResponse } from "./service_pb"; @@ -404,6 +406,17 @@ export interface ITerminalService extends grpc.UntypedServiceImplementation { * @generated from protobuf rpc: ConnectToDesktop(stream teleport.lib.teleterm.v1.ConnectToDesktopRequest) returns (stream teleport.lib.teleterm.v1.ConnectToDesktopResponse); */ connectToDesktop: grpc.handleBidiStreamingCall; + /** + * AttachDirectoryToDesktopSession opens a directory for a desktop session and enables file system operations for it. + * If there is no active desktop session associated with the specified desktop_uri and login, + * the RPC returns an error. + * + * This RPC does not automatically share the directory with the server (it does not send a SharedDirectoryAnnounce message). + * It only registers file system handlers for processing file system-related TDP events. + * + * @generated from protobuf rpc: AttachDirectoryToDesktopSession(teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionRequest) returns (teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionResponse); + */ + attachDirectoryToDesktopSession: grpc.handleUnaryCall; } /** * @grpc/grpc-js definition for the protobuf service teleport.lib.teleterm.v1.TerminalService. @@ -836,5 +849,15 @@ export const terminalServiceDefinition: grpc.ServiceDefinition requestDeserialize: bytes => ConnectToDesktopRequest.fromBinary(bytes), responseSerialize: value => Buffer.from(ConnectToDesktopResponse.toBinary(value)), requestSerialize: value => Buffer.from(ConnectToDesktopRequest.toBinary(value)) + }, + attachDirectoryToDesktopSession: { + path: "/teleport.lib.teleterm.v1.TerminalService/AttachDirectoryToDesktopSession", + originalName: "AttachDirectoryToDesktopSession", + requestStream: false, + responseStream: false, + responseDeserialize: bytes => AttachDirectoryToDesktopSessionResponse.fromBinary(bytes), + requestDeserialize: bytes => AttachDirectoryToDesktopSessionRequest.fromBinary(bytes), + responseSerialize: value => Buffer.from(AttachDirectoryToDesktopSessionResponse.toBinary(value)), + requestSerialize: value => Buffer.from(AttachDirectoryToDesktopSessionRequest.toBinary(value)) } }; diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts index bfdc0c4f2b4a3..683cbe49c95ec 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts @@ -1256,6 +1256,38 @@ export interface ConnectToDesktopResponse { */ data: Uint8Array; } +/** + * Request for AttachDirectoryToDesktopSession. + * + * @generated from protobuf message teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionRequest + */ +export interface AttachDirectoryToDesktopSessionRequest { + /** + * URI of the desktop. + * + * @generated from protobuf field: string desktop_uri = 1; + */ + desktopUri: string; + /** + * Login for the desktop session. + * + * @generated from protobuf field: string login = 2; + */ + login: string; + /** + * Path to share with a remote machine. Must be a directory. + * + * @generated from protobuf field: string path = 3; + */ + path: string; +} +/** + * Response for AttachDirectoryToDesktopSession. + * + * @generated from protobuf message teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionResponse + */ +export interface AttachDirectoryToDesktopSessionResponse { +} /** * PasswordlessPrompt describes different prompts we need from users * during the passwordless login flow. @@ -5571,6 +5603,94 @@ class ConnectToDesktopResponse$Type extends MessageType { + constructor() { + super("teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionRequest", [ + { no: 1, name: "desktop_uri", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "login", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "path", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): AttachDirectoryToDesktopSessionRequest { + const message = globalThis.Object.create((this.messagePrototype!)); + message.desktopUri = ""; + message.login = ""; + message.path = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AttachDirectoryToDesktopSessionRequest): AttachDirectoryToDesktopSessionRequest { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* string desktop_uri */ 1: + message.desktopUri = reader.string(); + break; + case /* string login */ 2: + message.login = reader.string(); + break; + case /* string path */ 3: + message.path = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: AttachDirectoryToDesktopSessionRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* string desktop_uri = 1; */ + if (message.desktopUri !== "") + writer.tag(1, WireType.LengthDelimited).string(message.desktopUri); + /* string login = 2; */ + if (message.login !== "") + writer.tag(2, WireType.LengthDelimited).string(message.login); + /* string path = 3; */ + if (message.path !== "") + writer.tag(3, WireType.LengthDelimited).string(message.path); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionRequest + */ +export const AttachDirectoryToDesktopSessionRequest = new AttachDirectoryToDesktopSessionRequest$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AttachDirectoryToDesktopSessionResponse$Type extends MessageType { + constructor() { + super("teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionResponse", []); + } + create(value?: PartialMessage): AttachDirectoryToDesktopSessionResponse { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AttachDirectoryToDesktopSessionResponse): AttachDirectoryToDesktopSessionResponse { + return target ?? this.create(); + } + internalBinaryWrite(message: AttachDirectoryToDesktopSessionResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message teleport.lib.teleterm.v1.AttachDirectoryToDesktopSessionResponse + */ +export const AttachDirectoryToDesktopSessionResponse = new AttachDirectoryToDesktopSessionResponse$Type(); /** * @generated ServiceType for protobuf service teleport.lib.teleterm.v1.TerminalService */ @@ -5616,5 +5736,6 @@ export const TerminalService = new ServiceType("teleport.lib.teleterm.v1.Termina { name: "UpdateUserPreferences", options: {}, I: UpdateUserPreferencesRequest, O: UpdateUserPreferencesResponse }, { name: "AuthenticateWebDevice", options: {}, I: AuthenticateWebDeviceRequest, O: AuthenticateWebDeviceResponse }, { name: "GetApp", options: {}, I: GetAppRequest, O: GetAppResponse }, - { name: "ConnectToDesktop", serverStreaming: true, clientStreaming: true, options: {}, I: ConnectToDesktopRequest, O: ConnectToDesktopResponse } + { name: "ConnectToDesktop", serverStreaming: true, clientStreaming: true, options: {}, I: ConnectToDesktopRequest, O: ConnectToDesktopResponse }, + { name: "AttachDirectoryToDesktopSession", options: {}, I: AttachDirectoryToDesktopSessionRequest, O: AttachDirectoryToDesktopSessionResponse } ]); diff --git a/lib/teleterm/apiserver/handler/handler_desktops.go b/lib/teleterm/apiserver/handler/handler_desktops.go index 42cdd2af018de..86a9206a01d94 100644 --- a/lib/teleterm/apiserver/handler/handler_desktops.go +++ b/lib/teleterm/apiserver/handler/handler_desktops.go @@ -17,6 +17,8 @@ package handler import ( + "context" + "github.com/gravitational/trace" "google.golang.org/grpc" @@ -61,6 +63,19 @@ func (s *Handler) ConnectToDesktop(stream grpc.BidiStreamingServer[api.ConnectTo return trace.Wrap(err) } - err = s.DaemonService.ConnectToDesktop(stream, parsed.GetClusterURI(), parsed.GetWindowsDesktopName(), login) + err = s.DaemonService.ConnectToDesktop(stream, parsed, login) return trace.Wrap(err) } + +// AttachDirectoryToDesktopSession opens a directory for a desktop session and enables file system operations for it. +// If there is no active desktop session associated with the specified desktop_uri and login, +// an error is returned. +func (s *Handler) AttachDirectoryToDesktopSession(ctx context.Context, in *api.AttachDirectoryToDesktopSessionRequest) (*api.AttachDirectoryToDesktopSessionResponse, error) { + parsed, err := uri.Parse(in.GetDesktopUri()) + if err != nil { + return &api.AttachDirectoryToDesktopSessionResponse{}, trace.Wrap(err) + } + + err = s.DaemonService.AttachDirectoryToDesktopSession(ctx, parsed, in.GetLogin(), in.GetPath()) + return &api.AttachDirectoryToDesktopSessionResponse{}, trace.Wrap(err) +} diff --git a/lib/teleterm/daemon/daemon.go b/lib/teleterm/daemon/daemon.go index d69da32ea305c..89ca7ba30c336 100644 --- a/lib/teleterm/daemon/daemon.go +++ b/lib/teleterm/daemon/daemon.go @@ -88,6 +88,7 @@ func New(cfg Config) (*Service, error) { closeContext: closeContext, cancel: cancel, gateways: make(map[string]gateway.Gateway), + desktopSessions: make(map[string]*desktop.Session), usageReporter: connectUsageReporter, headlessWatcherClosers: make(map[string]context.CancelFunc), headlessAuthSemaphore: newWaitSemaphore(maxConcurrentImportantModals, imporantModalWaitDuraiton), @@ -209,10 +210,10 @@ func (s *Service) AddCluster(ctx context.Context, webProxyAddress string) (*clus } // ConnectToDesktop establishes a desktop connection. -func (s *Service) ConnectToDesktop(stream grpc.BidiStreamingServer[api.ConnectToDesktopRequest, api.ConnectToDesktopResponse], uri uri.ResourceURI, desktopName, login string) error { +func (s *Service) ConnectToDesktop(stream grpc.BidiStreamingServer[api.ConnectToDesktopRequest, api.ConnectToDesktopResponse], desktopURI uri.ResourceURI, login string) error { ctx := stream.Context() - cluster, clusterClient, err := s.ResolveClusterURI(uri) + cluster, clusterClient, err := s.ResolveClusterURI(desktopURI) if err != nil { return trace.Wrap(err) } @@ -222,12 +223,46 @@ func (s *Service) ConnectToDesktop(stream grpc.BidiStreamingServer[api.ConnectTo return trace.Wrap(err) } + session, cleanup, err := s.newDesktopSession(desktopURI, login) + if err != nil { + return trace.Wrap(err) + } + defer cleanup() + err = clusters.AddMetadataToRetryableError(ctx, func() error { - return trace.Wrap(desktop.Connect(ctx, stream, clusterClient, cachedClient.ProxyClient, desktopName, login)) + return trace.Wrap(session.Start(ctx, stream, clusterClient, cachedClient.ProxyClient)) }) return trace.Wrap(err) } +// TODO Comment, returns cleanup function. +func (s *Service) newDesktopSession(desktopURI uri.ResourceURI, login string) (*desktop.Session, func(), error) { + s.desktopSessionsMu.Lock() + defer s.desktopSessionsMu.Unlock() + + key := desktopSessionKey(desktopURI, login) + + if _, ok := s.desktopSessions[key]; ok { + return nil, nil, trace.AlreadyExists("TODO: Error message") + } + + session, err := desktop.NewSession(desktopURI, login) + if err != nil { + return nil, nil, trace.Wrap(err) + } + + s.desktopSessions[key] = session + + cleanup := func() { + s.desktopSessionsMu.Lock() + defer s.desktopSessionsMu.Unlock() + + delete(s.desktopSessions, key) + } + + return session, cleanup, nil +} + // RemoveCluster removes cluster func (s *Service) RemoveCluster(ctx context.Context, uri string) error { cluster, _, err := s.ResolveCluster(uri) @@ -1234,6 +1269,26 @@ func (s *Service) ClearCachedClientsForRoot(clusterURI uri.ResourceURI) error { return trace.Wrap(s.clientCache.ClearForRoot(profileName)) } +// AttachDirectoryToDesktopSession opens a directory for a desktop session and enables file system operations for it. +// If there is no active desktop session associated with the specified desktop_uri and login, +// an error is returned. +func (s *Service) AttachDirectoryToDesktopSession(_ context.Context, desktopURI uri.ResourceURI, login, path string) error { + s.desktopSessionsMu.Lock() + defer s.desktopSessionsMu.Unlock() + + session, ok := s.desktopSessions[desktopSessionKey(desktopURI, login)] + if !ok { + return trace.BadParameter("TODO: Error message") + } + + err := session.AttachSharedDirectory(path) + return trace.Wrap(err) +} + +func desktopSessionKey(desktopURI uri.ResourceURI, login string) string { + return desktopURI.String() + "-" + login +} + // Service is the daemon service type Service struct { cfg *Config @@ -1249,6 +1304,11 @@ type Service struct { // gatewaysMu guards gateways. gatewaysMu sync.RWMutex + // TODO: Comments, explain why a map is needed in the first place (two different RPCs, one needs + // to deliver something to a struct created in another). + desktopSessions map[string]*desktop.Session + desktopSessionsMu sync.Mutex + // The Electron App can display multiple important modals by showing the latest one and hiding the others. // However, we should be careful with it, and generally try to limit the number of prompts on the tshd side, // to avoid flooding the app. diff --git a/lib/teleterm/services/desktop/desktop.go b/lib/teleterm/services/desktop/desktop.go index 8df386edefaa6..5e3e5fe444fd3 100644 --- a/lib/teleterm/services/desktop/desktop.go +++ b/lib/teleterm/services/desktop/desktop.go @@ -18,6 +18,9 @@ package desktop import ( "context" + "errors" + "os" + "sync" "github.com/gravitational/trace" "google.golang.org/grpc" @@ -28,23 +31,77 @@ import ( api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/srv/desktop/tdp" + "github.com/gravitational/teleport/lib/teleterm/api/uri" ) +// Session uniquely describes a desktop session. +// There can be only one session for the given desktop and login pair. +type Session struct { + desktopURI uri.ResourceURI + login string + + dirAccess *DirectoryAccess + dirAccessMu sync.RWMutex +} + +func NewSession(desktopURI uri.ResourceURI, login string) (*Session, error) { + if desktopURI.GetWindowsDesktopName() == "" { + return nil, trace.BadParameter("invalid desktop URI %q", desktopURI) + } + + return &Session{ + desktopURI: desktopURI, + login: login, + }, nil +} + +func (s *Session) desktopName() string { + return s.desktopURI.GetWindowsDesktopName() +} + +func (s *Session) AttachSharedDirectory(basePath string) error { + s.dirAccessMu.Lock() + defer s.dirAccessMu.Unlock() + + if s.dirAccess != nil { + return trace.AlreadyExists("TODO: error message") + } + + dirAccess, err := NewDirectoryAccess(basePath) + if err != nil { + return trace.Wrap(err) + } + s.dirAccess = dirAccess + + return nil +} + +func (s *Session) GetDirectoryAccess() (*DirectoryAccess, error) { + s.dirAccessMu.RLock() + s.dirAccessMu.RUnlock() + + if s.dirAccess == nil { + return nil, trace.Errorf("TODO: error message") + } + + return s.dirAccess, nil +} + // Connect starts a remote desktop session. -func Connect(ctx context.Context, stream grpc.BidiStreamingServer[api.ConnectToDesktopRequest, api.ConnectToDesktopResponse], clusterClient *client.TeleportClient, proxyClient *proxy.Client, desktopName, login string) error { +func (s *Session) Start(ctx context.Context, stream grpc.BidiStreamingServer[api.ConnectToDesktopRequest, api.ConnectToDesktopResponse], clusterClient *client.TeleportClient, proxyClient *proxy.Client) error { keyRing, err := clusterClient.IssueUserCertsWithMFA(ctx, client.ReissueParams{ RouteToCluster: clusterClient.SiteName, TTL: clusterClient.KeyTTL, RouteToWindowsDesktop: proto.RouteToWindowsDesktop{ - WindowsDesktop: desktopName, - Login: login, + WindowsDesktop: s.desktopName(), + Login: s.login, }, }) if err != nil { return trace.Wrap(err) } - cert, err := keyRing.WindowsDesktopTLSCert(desktopName) + cert, err := keyRing.WindowsDesktopTLSCert(s.desktopName()) if err != nil { return trace.Wrap(err) } @@ -54,7 +111,7 @@ func Connect(ctx context.Context, stream grpc.BidiStreamingServer[api.ConnectToD return trace.Wrap(err) } - conn, err := proxyClient.ProxyWindowsDesktopSession(ctx, clusterClient.SiteName, desktopName, cert, tlsConfig.RootCAs) + conn, err := proxyClient.ProxyWindowsDesktopSession(ctx, clusterClient.SiteName, s.desktopName(), cert, tlsConfig.RootCAs) if err != nil { return trace.Wrap(err) } @@ -64,7 +121,7 @@ func Connect(ctx context.Context, stream grpc.BidiStreamingServer[api.ConnectToD // send the username. tdpConn := tdp.NewConn(conn) defer tdpConn.Close() - err = tdpConn.WriteMessage(tdp.ClientUsername{Username: login}) + err = tdpConn.WriteMessage(tdp.ClientUsername{Username: s.login}) if err != nil { return trace.Wrap(err) } @@ -76,7 +133,24 @@ func Connect(ctx context.Context, stream grpc.BidiStreamingServer[api.ConnectToD return trace.Wrap(err) } - tdpConnProxy := tdp.NewConnProxy(downstreamRW, conn, nil) + interceptor := serverInterceptor{ + directoryAccessProvider: s, + } + + tdpConnProxy := tdp.NewConnProxy(downstreamRW, conn, func(tdpConn *tdp.Conn, message tdp.Message) (tdp.Message, error) { + msg, intErr := interceptor.process(message, func(message tdp.Message) error { + return trace.Wrap(tdpConn.WriteMessage(message)) + }) + if intErr != nil { + // Treat all file system errors as warnings, do not interrupt the connection. + return tdp.Alert{ + Message: intErr.Error(), + Severity: tdp.SeverityWarning, + }, nil + } + return msg, nil + }) + return trace.Wrap(tdpConnProxy.Run(ctx)) } @@ -105,5 +179,250 @@ func (d clientStream) Recv() ([]byte, error) { return nil, trace.BadParameter("received invalid message") } + // Check if the message sent from the renderer is allowed. + decoded, err := tdp.Decode(data) + if err != nil { + return nil, trace.Wrap(err, "could not decode desktop message") + } + err = isClientMessageAllowed(decoded) + if err != nil { + return nil, trace.Wrap(err, "disallowed desktop message") + } + return data, nil } + +// isClientMessageAllowed checks whether a message from the client is allowed +// to be forwarded to the server. +// +// Responses related to shared directory operations are handled exclusively +// by tshd and should not originate from the renderer process. +func isClientMessageAllowed(msg tdp.Message) error { + switch msg.(type) { + case tdp.SharedDirectoryInfoResponse, + tdp.SharedDirectoryCreateResponse, + tdp.SharedDirectoryDeleteResponse, + tdp.SharedDirectoryReadResponse, + tdp.SharedDirectoryWriteResponse, + tdp.SharedDirectoryMoveResponse, + tdp.SharedDirectoryListResponse, + tdp.SharedDirectoryTruncateResponse: + return trace.AccessDenied("file system messages are not allowed from the renderer process") + default: + return nil + } +} + +// serverInterceptor intercepts and processes messages sent from the server to the client. +type serverInterceptor struct { + directoryAccessProvider directoryAccessProvider +} + +type directoryAccessProvider interface { + GetDirectoryAccess() (*DirectoryAccess, error) +} + +func (d *serverInterceptor) process(msg tdp.Message, sendToServer func(message tdp.Message) error) (tdp.Message, error) { + switch r := msg.(type) { + case tdp.SharedDirectoryInfoRequest: + return nil, trace.Wrap(d.handleSharedDirectoryInfoRequest(r, sendToServer)) + case tdp.SharedDirectoryListRequest: + return nil, trace.Wrap(d.handleSharedDirectoryListRequest(r, sendToServer)) + case tdp.SharedDirectoryReadRequest: + return nil, trace.Wrap(d.handleSharedDirectoryReadRequest(r, sendToServer)) + case tdp.SharedDirectoryMoveRequest: + return nil, trace.Wrap(d.handleSharedDirectoryMoveRequest(r, sendToServer)) + case tdp.SharedDirectoryWriteRequest: + return nil, trace.Wrap(d.handleSharedDirectoryWriteRequest(r, sendToServer)) + case tdp.SharedDirectoryTruncateRequest: + return nil, trace.Wrap(d.handleSharedDirectoryTruncateRequest(r, sendToServer)) + case tdp.SharedDirectoryCreateRequest: + return nil, trace.Wrap(d.handleSharedDirectoryCreateRequest(r, sendToServer)) + case tdp.SharedDirectoryDeleteRequest: + return nil, trace.Wrap(d.handleSharedDirectoryDeleteRequest(r, sendToServer)) + default: + return msg, nil + } +} + +type SharedDirectoryErrCode uint32 + +const ( + SharedDirectoryErrCodeNil SharedDirectoryErrCode = iota + SharedDirectoryErrCodeFailed + SharedDirectoryErrCodeDoesNotExist + SharedDirectoryErrCodeAlreadyExists +) + +func (d *serverInterceptor) handleSharedDirectoryInfoRequest(r tdp.SharedDirectoryInfoRequest, sendToServer func(message tdp.Message) error) error { + dirAccess, err := d.directoryAccessProvider.GetDirectoryAccess() + if err != nil { + return trace.Wrap(err) + } + + info, err := dirAccess.Stat(r.Path) + if err == nil { + return trace.Wrap(sendToServer(tdp.SharedDirectoryInfoResponse{ + CompletionID: r.CompletionID, + ErrCode: uint32(SharedDirectoryErrCodeNil), + Fso: toFso(info), + })) + } + if errors.Is(err, os.ErrNotExist) { + return trace.Wrap(sendToServer(tdp.SharedDirectoryInfoResponse{ + CompletionID: r.CompletionID, + ErrCode: uint32(SharedDirectoryErrCodeDoesNotExist), + Fso: tdp.FileSystemObject{ + LastModified: 0, + Size: 0, + FileType: 0, + IsEmpty: 0, + Path: "", + }})) + } + return trace.Wrap(err) +} + +func (d *serverInterceptor) handleSharedDirectoryListRequest(r tdp.SharedDirectoryListRequest, sendToServer func(message tdp.Message) error) error { + dirAccess, err := d.directoryAccessProvider.GetDirectoryAccess() + if err != nil { + return trace.Wrap(err) + } + contents, err := dirAccess.ReadDir(r.Path) + if err != nil { + return trace.Wrap(err) + } + + fsoList := make([]tdp.FileSystemObject, len(contents)) + for i, content := range contents { + fsoList[i] = toFso(content) + } + + err = sendToServer(tdp.SharedDirectoryListResponse{ + CompletionID: r.CompletionID, + ErrCode: uint32(SharedDirectoryErrCodeNil), + FsoList: fsoList, + }) + return trace.Wrap(err) +} + +func (d *serverInterceptor) handleSharedDirectoryReadRequest(r tdp.SharedDirectoryReadRequest, sendToServer func(message tdp.Message) error) error { + dirAccess, err := d.directoryAccessProvider.GetDirectoryAccess() + if err != nil { + return trace.Wrap(err) + } + contents, err := dirAccess.Read(r.Path, int64(r.Offset), r.Length) + if err != nil { + return trace.Wrap(err) + } + + err = sendToServer(tdp.SharedDirectoryReadResponse{ + CompletionID: r.CompletionID, + ErrCode: uint32(SharedDirectoryErrCodeNil), + ReadDataLength: uint32(len(contents)), + ReadData: contents, + }) + return trace.Wrap(err) +} + +func (d *serverInterceptor) handleSharedDirectoryMoveRequest(r tdp.SharedDirectoryMoveRequest, sendToServer func(message tdp.Message) error) error { + err := sendToServer(tdp.SharedDirectoryMoveResponse{ + CompletionID: r.CompletionID, + ErrCode: uint32(SharedDirectoryErrCodeFailed), + }) + if err != nil { + return trace.Wrap(err) + } + + return trace.NotImplemented("Moving or renaming files and directories within a shared directory is not supported.") +} + +func (d *serverInterceptor) handleSharedDirectoryWriteRequest(r tdp.SharedDirectoryWriteRequest, sendToServer func(message tdp.Message) error) error { + dirAccess, err := d.directoryAccessProvider.GetDirectoryAccess() + if err != nil { + return trace.Wrap(err) + } + bytesWritten, err := dirAccess.Write(r.Path, int64(r.Offset), r.WriteData) + if err != nil { + return trace.Wrap(err) + } + + err = sendToServer(tdp.SharedDirectoryWriteResponse{ + CompletionID: r.CompletionID, + ErrCode: uint32(SharedDirectoryErrCodeNil), + BytesWritten: uint32(bytesWritten), + }) + return trace.Wrap(err) +} + +func (d *serverInterceptor) handleSharedDirectoryTruncateRequest(r tdp.SharedDirectoryTruncateRequest, sendToServer func(message tdp.Message) error) error { + dirAccess, err := d.directoryAccessProvider.GetDirectoryAccess() + if err != nil { + return trace.Wrap(err) + } + err = dirAccess.Truncate(r.Path, int64(r.EndOfFile)) + if err != nil { + return trace.Wrap(err) + } + + err = sendToServer(tdp.SharedDirectoryTruncateResponse{ + CompletionID: r.CompletionID, + ErrCode: uint32(SharedDirectoryErrCodeNil), + }) + return trace.Wrap(err) +} + +func (d *serverInterceptor) handleSharedDirectoryCreateRequest(r tdp.SharedDirectoryCreateRequest, sendToServer func(message tdp.Message) error) error { + dirAccess, err := d.directoryAccessProvider.GetDirectoryAccess() + if err != nil { + return trace.Wrap(err) + } + err = dirAccess.Create(r.Path, FileType(r.FileType)) + if err != nil { + return trace.Wrap(err) + } + + info, err := dirAccess.Stat(r.Path) + if err != nil { + return trace.Wrap(err) + } + + err = sendToServer(tdp.SharedDirectoryCreateResponse{ + CompletionID: r.CompletionID, + ErrCode: uint32(SharedDirectoryErrCodeNil), + Fso: toFso(info), + }) + return trace.Wrap(err) +} + +func (d *serverInterceptor) handleSharedDirectoryDeleteRequest(r tdp.SharedDirectoryDeleteRequest, sendToServer func(message tdp.Message) error) error { + dirAccess, err := d.directoryAccessProvider.GetDirectoryAccess() + if err != nil { + return trace.Wrap(err) + } + err = dirAccess.Delete(r.Path) + if err != nil { + return trace.Wrap(err) + } + + err = sendToServer(tdp.SharedDirectoryDeleteResponse{ + CompletionID: r.CompletionID, + ErrCode: uint32(SharedDirectoryErrCodeNil), + }) + return trace.Wrap(err) +} + +func toFso(info *FileOrDirInfo) tdp.FileSystemObject { + obj := tdp.FileSystemObject{ + LastModified: uint64(info.LastModified), + Size: uint64(info.Size), + FileType: uint32(info.FileType), + IsEmpty: 1, + Path: info.Path, + } + if info.IsEmpty { + obj.IsEmpty = 0 + } + + return obj +} diff --git a/lib/teleterm/services/desktop/directorysharing.go b/lib/teleterm/services/desktop/directorysharing.go new file mode 100644 index 0000000000000..b010bba05af73 --- /dev/null +++ b/lib/teleterm/services/desktop/directorysharing.go @@ -0,0 +1,296 @@ +// Teleport +// Copyright (C) 2025 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 desktop + +import ( + "errors" + "io" + "os" + "path/filepath" + "strings" + + "github.com/gravitational/trace" +) + +// DirectoryAccess allows access to the shared directory. +// Should be kept in sync with web/packages/shared/libs/tdp/sharedDirectoryAccess.ts +// where FS events are handled for Web UI. +type DirectoryAccess struct { + // safePathGetter allows building a safe path by joining the shared directory path + // and a relative path. + // The shared directory path is not exposed to avoid unsafe operations. + // + // TODO(gzdunek): This code can be greatly simplified with os.OpenRoot. + // Switch to it when branch/v17 is updated to Go 1.24. + basePath string +} + +// FileOrDirInfo contains metadata about a file or a directory. +type FileOrDirInfo struct { + Size int64 + LastModified int64 + FileType FileType // "file" or "directory" + IsEmpty bool + Path string +} + +type FileType uint32 + +const ( + FileTypeFile FileType = iota + FileTypeDir +) + +const StandardDirSize = 4096 + +func NewDirectoryAccess(baseDir string) (*DirectoryAccess, error) { + basePath, err := filepath.EvalSymlinks(baseDir) + if err != nil { + return nil, trace.Wrap(err) + } + + stat, err := os.Stat(basePath) + if err != nil { + return nil, trace.Wrap(err) + } + if !stat.IsDir() { + return nil, trace.BadParameter("%q is not a directory", baseDir) + } + + return &DirectoryAccess{ + basePath: basePath, + }, nil + +} + +func (s *DirectoryAccess) safePathGetter(relativePath string) (string, error) { + full := filepath.Join(s.basePath, relativePath) + resolved, err := filepath.EvalSymlinks(full) + if err != nil { + // EvalSymlinks returns an error if the target file does not exist. + // In that case, attempt to resolve the symlinks of the parent directory instead. + if os.IsNotExist(err) { + parent := filepath.Dir(full) + resolvedParent, perr := filepath.EvalSymlinks(parent) + if perr != nil { + return "", trace.Wrap(perr) + } + + // Reconstruct the full path by joining the resolved parent with the original file name. + resolved = filepath.Join(resolvedParent, filepath.Base(full)) + } else { + return "", trace.Wrap(err) + } + } + if !isSubPath(s.basePath, resolved) { + return "", trace.BadParameter("path escapes from parent") + } + return resolved, nil +} + +func isSubPath(parent, child string) bool { + return child == parent || strings.HasPrefix(child, parent+string(filepath.Separator)) +} + +// Stat retrieves metadata about a file or directory at the given path. +func (s *DirectoryAccess) Stat(relativePath string) (*FileOrDirInfo, error) { + path, err := s.safePathGetter(relativePath) + if err != nil { + return nil, trace.Wrap(err) + } + + stat, err := os.Stat(path) + if err != nil { + return nil, trace.Wrap(err) + } + + info, err := s.readFileOrDirInfo(relativePath, stat) + return info, trace.Wrap(err) +} + +// ReadDir lists files and directories within the given directory path, skips symlinks. +func (s *DirectoryAccess) ReadDir(relativePath string) ([]*FileOrDirInfo, error) { + path, err := s.safePathGetter(relativePath) + if err != nil { + return nil, trace.Wrap(err) + } + + entries, err := os.ReadDir(path) + if err != nil { + return nil, trace.Wrap(err) + } + + var results []*FileOrDirInfo + for _, entry := range entries { + fileInfo, err := entry.Info() + if err != nil { + return nil, trace.Wrap(err) + } + + // Skip symlinks, we can't present them properly in the remote machine. + if fileInfo.Mode().Type()&os.ModeSymlink != 0 { + continue + } + + entryRelativePath := filepath.Join(relativePath, fileInfo.Name()) + fileOrDir, err := s.readFileOrDirInfo(entryRelativePath, fileInfo) + if err != nil { + return nil, trace.Wrap(err) + } + + results = append(results, fileOrDir) + } + + return results, nil +} + +// Read reads a slice of a file. +func (s *DirectoryAccess) Read(relativePath string, offset int64, length uint32) ([]byte, error) { + path, err := s.safePathGetter(relativePath) + if err != nil { + return nil, trace.Wrap(err) + } + + opened, err := os.Open(path) + if err != nil { + return nil, err + } + defer opened.Close() + + buf := make([]byte, length) + _, err = opened.Seek(offset, io.SeekStart) + if err != nil { + return nil, err + } + + n, err := opened.Read(buf) + if err != nil && !errors.Is(err, io.EOF) { + return nil, trace.Wrap(err) + } + return buf[:n], nil +} + +// Write writes data to a file at a given offset. +func (s *DirectoryAccess) Write(relativePath string, offset int64, data []byte) (int, error) { + path, err := s.safePathGetter(relativePath) + if err != nil { + return 0, trace.Wrap(err) + } + + file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return 0, err + } + defer file.Close() + + _, err = file.Seek(offset, io.SeekStart) + if err != nil { + return 0, err + } + + n, err := file.Write(data) + if err != nil { + return 0, trace.Wrap(err) + } + + return n, nil +} + +// Truncate truncates a file to the specified size. +func (s *DirectoryAccess) Truncate(relativePath string, size int64) error { + path, err := s.safePathGetter(relativePath) + if err != nil { + return trace.Wrap(err) + } + + return trace.Wrap(os.Truncate(path, size)) +} + +// Create creates a new file or directory at the given path. +func (s *DirectoryAccess) Create(relativePath string, fileType FileType) error { + path, err := s.safePathGetter(relativePath) + if err != nil { + return trace.Wrap(err) + } + + switch fileType { + case FileTypeFile: + file, err := os.Create(path) + if err != nil { + if os.IsExist(err) { + return nil // Ignore if file already exists + } + return err + } + return file.Close() + case FileTypeDir: + err := os.Mkdir(path, 0700) + if os.IsExist(err) { + return nil // Ignore if directory already exists + } + return err + default: + return errors.New("unknown file type") + } +} + +// Delete removes a file or directory at the given path. +func (s *DirectoryAccess) Delete(relativePath string) error { + path, err := s.safePathGetter(relativePath) + if err != nil { + return trace.Wrap(err) + } + + err = os.RemoveAll(path) + return trace.Wrap(err) +} + +func (s *DirectoryAccess) readFileOrDirInfo(relativePath string, f os.FileInfo) (*FileOrDirInfo, error) { + path, err := s.safePathGetter(relativePath) + if err != nil { + return nil, trace.Wrap(err) + } + + info := &FileOrDirInfo{ + Size: f.Size(), + LastModified: f.ModTime().Unix(), + Path: relativePath, + IsEmpty: false, + } + + if f.IsDir() { + r, err := os.Open(path) + if err != nil { + return nil, trace.Wrap(err) + } + defer r.Close() + info.FileType = FileTypeDir + // Read up to one entry. + if _, err := r.Readdirnames(1); err != nil { + if errors.Is(err, io.EOF) { + info.IsEmpty = true + } else { + return nil, trace.Wrap(err) + } + } + info.Size = StandardDirSize + } else { + info.FileType = FileTypeFile + } + + return info, nil +} diff --git a/lib/teleterm/services/desktop/directorysharing_test.go b/lib/teleterm/services/desktop/directorysharing_test.go new file mode 100644 index 0000000000000..564ed77498239 --- /dev/null +++ b/lib/teleterm/services/desktop/directorysharing_test.go @@ -0,0 +1,220 @@ +// Teleport +// Copyright (C) 2025 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 desktop + +import ( + "os" + "path/filepath" + "testing" + + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/lib/teleterm/api/uri" +) + +var targetDesktop = TargetSession{ + DesktopURI: uri.NewClusterURI("foo").AppendWindowsDesktop("bar"), + Login: "admin", +} + +func TestRegisterSharedDirectory(t *testing.T) { + manager := NewDirectorySharingManager() + + // Clean state, register the first handler. + _, unregister, err := manager.Register(targetDesktop) + require.NoError(t, err) + // Remove the registered handler. + unregister() + + // Register the handler again. + _, unregister, err = manager.Register(targetDesktop) + require.NoError(t, err) + // Do not unregister it immediately. + defer unregister() + + // Try to register the handler again, it should return the error. + _, _, err = manager.Register(targetDesktop) + require.True(t, trace.IsAlreadyExists(err)) +} + +func TestGetSharedDirectory(t *testing.T) { + manager := NewDirectorySharingManager() + + _, unregister, err := manager.Register(targetDesktop) + require.NoError(t, err) + + access, err := manager.Get(targetDesktop) + require.NoError(t, err) + require.NotNil(t, access) + + // Remove the registered handler. + unregister() + + _, err = manager.Get(targetDesktop) + require.True(t, trace.IsNotFound(err)) +} + +func TestOpenSharedDirectory(t *testing.T) { + path := t.TempDir() + filePath := filepath.Join(path, testFilename) + err := os.WriteFile(filePath, []byte("test"), 0600) + require.NoError(t, err) + access := DirectoryAccess{} + err = access.Open(filePath) + require.True(t, trace.IsBadParameter(err), "%q is not a directory", filePath) +} + +const ( + testDirname = "test_dir" + testFilename = "test_file" + testSymlinkFilename = "test_symlink" +) + +func setUpSharedDir(t *testing.T) (*DirectoryAccess, string) { + path := t.TempDir() + err := os.Mkdir(filepath.Join(path, testDirname), 0700) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(path, testFilename), []byte("test"), 0600) + require.NoError(t, err) + err = os.Symlink(filepath.Join(path, testFilename), filepath.Join(path, testSymlinkFilename)) + require.NoError(t, err) + access := DirectoryAccess{} + err = access.Open(path) + require.NoError(t, err) + return &access, path +} + +func TestDirectoryAccessEscapingPaths(t *testing.T) { + outOfRootPath := filepath.Join(testDirname, "../..") + tests := []struct { + name string + call func(*DirectoryAccess) error + }{ + {"Stat", func(a *DirectoryAccess) error { _, err := a.Stat(outOfRootPath); return err }}, + {"ReadDir", func(a *DirectoryAccess) error { _, err := a.ReadDir(outOfRootPath); return err }}, + {"Read", func(a *DirectoryAccess) error { _, err := a.Read(outOfRootPath, 0, 100); return err }}, + {"Write", func(a *DirectoryAccess) error { _, err := a.Write(outOfRootPath, 0, []byte("test")); return err }}, + {"Truncate", func(a *DirectoryAccess) error { err := a.Truncate(outOfRootPath, 100); return err }}, + {"Create", func(a *DirectoryAccess) error { err := a.Create(outOfRootPath, FileTypeDir); return err }}, + {"Delete", func(a *DirectoryAccess) error { err := a.Delete(outOfRootPath); return err }}, + } + + for _, tt := range tests { + t.Run(tt.name+"_Escape", func(t *testing.T) { + t.Helper() + access, _ := setUpSharedDir(t) + err := tt.call(access) + require.ErrorContains(t, err, "path escapes from parent") + }) + } +} + +func TestDirectoryAccessSuccessOperations(t *testing.T) { + t.Run("Stat", func(t *testing.T) { + access, path := setUpSharedDir(t) + info, err := access.Stat("") + require.NoError(t, err) + + osStat, err := os.Stat(path) + require.NoError(t, err) + + require.Equal(t, &FileOrDirInfo{ + Size: 4096, + LastModified: osStat.ModTime().Unix(), + FileType: FileTypeDir, + IsEmpty: false, + Path: "", + }, info) + }) + + t.Run("ReadDir", func(t *testing.T) { + access, path := setUpSharedDir(t) + dir, err := access.ReadDir("") + require.NoError(t, err) + + require.Len(t, dir, 2) + osStat, err := os.Stat(filepath.Join(path, testDirname)) + require.NoError(t, err) + require.Contains(t, dir, &FileOrDirInfo{ + Size: 4096, + LastModified: osStat.ModTime().Unix(), + FileType: FileTypeDir, + IsEmpty: true, + Path: testDirname, + }) + osStat, err = os.Stat(filepath.Join(path, testFilename)) + require.NoError(t, err) + require.Contains(t, dir, &FileOrDirInfo{ + Size: osStat.Size(), + LastModified: osStat.ModTime().Unix(), + FileType: FileTypeFile, + IsEmpty: false, + Path: testFilename, + }) + }) + + t.Run("Read", func(t *testing.T) { + access, _ := setUpSharedDir(t) + read, err := access.Read(testFilename, 0, 100) + require.NoError(t, err) + require.Equal(t, []byte("test"), read) + }) + + t.Run("Write", func(t *testing.T) { + access, _ := setUpSharedDir(t) + written, err := access.Write(testFilename, 4, []byte("_new_content")) + require.NoError(t, err) + require.Equal(t, 12, written) + + read, err := access.Read(testFilename, 0, 100) + require.NoError(t, err) + require.Equal(t, []byte("test_new_content"), read) + }) + + t.Run("Truncate", func(t *testing.T) { + access, _ := setUpSharedDir(t) + err := access.Truncate(testFilename, 100) + require.NoError(t, err) + stat, err := access.Stat(testFilename) + require.NoError(t, err) + require.Equal(t, int64(100), stat.Size) + }) + + t.Run("Create", func(t *testing.T) { + access, _ := setUpSharedDir(t) + + err := access.Create("new_file", FileTypeFile) + require.NoError(t, err) + createdFile, err := access.Stat("new_file") + require.NoError(t, err) + require.Equal(t, FileTypeFile, createdFile.FileType) + + err = access.Create("new_dir", FileTypeDir) + require.NoError(t, err) + createdDir, err := access.Stat("new_dir") + require.NoError(t, err) + require.Equal(t, FileTypeDir, createdDir.FileType) + }) + + t.Run("Delete", func(t *testing.T) { + access, _ := setUpSharedDir(t) + err := access.Delete(testFilename) + require.NoError(t, err) + require.NoFileExists(t, testFilename) + }) +} diff --git a/proto/teleport/lib/teleterm/v1/service.proto b/proto/teleport/lib/teleterm/v1/service.proto index b8192f6fe5b66..f359340673923 100644 --- a/proto/teleport/lib/teleterm/v1/service.proto +++ b/proto/teleport/lib/teleterm/v1/service.proto @@ -185,6 +185,13 @@ service TerminalService { // ConnectToDesktop is a bidirectional stream for the desktop connection. rpc ConnectToDesktop(stream ConnectToDesktopRequest) returns (stream ConnectToDesktopResponse); + // AttachDirectoryToDesktopSession opens a directory for a desktop session and enables file system operations for it. + // If there is no active desktop session associated with the specified desktop_uri and login, + // the RPC returns an error. + // + // This RPC does not automatically share the directory with the server (it does not send a SharedDirectoryAnnounce message). + // It only registers file system handlers for processing file system-related TDP events. + rpc AttachDirectoryToDesktopSession(AttachDirectoryToDesktopSessionRequest) returns (AttachDirectoryToDesktopSessionResponse); } message EmptyResponse {} @@ -700,3 +707,16 @@ message ConnectToDesktopResponse { // Data is a TDP (Teleport Desktop Protocol) message sent from the desktop service to the client. bytes data = 1; } + +// Request for AttachDirectoryToDesktopSession. +message AttachDirectoryToDesktopSessionRequest { + // URI of the desktop. + string desktop_uri = 1; + // Login for the desktop session. + string login = 2; + // Path to share with a remote machine. Must be a directory. + string path = 3; +} + +// Response for AttachDirectoryToDesktopSession. +message AttachDirectoryToDesktopSessionResponse {} diff --git a/web/packages/shared/libs/tdp/sharedDirectoryAccess.ts b/web/packages/shared/libs/tdp/sharedDirectoryAccess.ts index dc9168c5f4902..ec25bbf07a8f1 100644 --- a/web/packages/shared/libs/tdp/sharedDirectoryAccess.ts +++ b/web/packages/shared/libs/tdp/sharedDirectoryAccess.ts @@ -42,6 +42,8 @@ export interface SharedDirectoryAccess { /** * Enables directory sharing using FileSystem API. * Most of the methods can potentially throw errors and so should be wrapped in try/catch blocks. + * Should be kept in sync with lib/teleterm/services/desktop/directorysharing.go + * where file system events are handled for Connect. */ export class BrowserFileSystem implements SharedDirectoryAccess { private dir: FileSystemDirectoryHandle | undefined; diff --git a/web/packages/teleterm/src/main.ts b/web/packages/teleterm/src/main.ts index 7b0586265cb7c..3c49b98dee58f 100644 --- a/web/packages/teleterm/src/main.ts +++ b/web/packages/teleterm/src/main.ts @@ -155,7 +155,7 @@ async function initializeApp(): Promise { const rootClusterProxyHostAllowList = new Set(); (async () => { - const tshdClient = await mainProcess.initTshdClient(); + const tshdClient = await mainProcess.getTshdClient(); manageRootClusterProxyHostAllowList({ tshdClient, diff --git a/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts b/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts index 897da5871a469..c4f5c0b07db7e 100644 --- a/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts +++ b/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts @@ -173,6 +173,10 @@ export class MockMainProcessClient implements MainProcessClient { } refreshClusterList() {} + + async selectDirectoryForDesktopSession() { + return ''; + } } export const makeRuntimeSettings = ( diff --git a/web/packages/teleterm/src/mainProcess/mainProcess.ts b/web/packages/teleterm/src/mainProcess/mainProcess.ts index a2d465e5e75e4..2637870d62b35 100644 --- a/web/packages/teleterm/src/mainProcess/mainProcess.ts +++ b/web/packages/teleterm/src/mainProcess/mainProcess.ts @@ -109,6 +109,7 @@ export default class MainProcess { ) ); private readonly agentRunner: AgentRunner; + private tshdClient: Promise; private constructor(opts: Options) { this.settings = opts.settings; @@ -170,12 +171,18 @@ export default class MainProcess { } } - async initTshdClient(): Promise { - const { tsh: tshdAddress } = await this.resolvedChildProcessAddresses; - return setUpTshdClient({ - runtimeSettings: this.settings, - tshdAddress, - }); + async getTshdClient(): Promise { + if (!this.tshdClient) { + this.tshdClient = this.resolvedChildProcessAddresses.then( + ({ tsh: tshdAddress }) => + setUpTshdClient({ + runtimeSettings: this.settings, + tshdAddress, + }) + ); + } + + return this.tshdClient; } private initTshd() { @@ -562,6 +569,31 @@ export default class MainProcess { } ); + ipcMain.handle( + MainProcessIpc.SelectDirectoryForDesktopSession, + async (_, args: { desktopUri: string; login: string }) => { + const value = await dialog.showOpenDialog({ + properties: ['openDirectory'], + }); + if (value.canceled) { + throw new Error('Selecting directory canceled.'); + } + if (value.filePaths.length !== 1) { + throw new Error('No directory selected.'); + } + + const [dirPath] = value.filePaths; + const tshClient = await this.getTshdClient(); + await tshClient.attachDirectoryToDesktopSession({ + desktopUri: args.desktopUri, + login: args.login, + path: dirPath, + }); + + return path.basename(dirPath); + } + ); + subscribeToTerminalContextMenuEvent(this.configService); subscribeToTabContextMenuEvent( this.settings.availableShells, diff --git a/web/packages/teleterm/src/mainProcess/mainProcessClient.ts b/web/packages/teleterm/src/mainProcess/mainProcessClient.ts index 87717b884f4c2..67b21fd49507e 100644 --- a/web/packages/teleterm/src/mainProcess/mainProcessClient.ts +++ b/web/packages/teleterm/src/mainProcess/mainProcessClient.ts @@ -190,5 +190,14 @@ export default function createMainProcessClient(): MainProcessClient { refreshClusterList() { ipcRenderer.send(MainProcessIpc.RefreshClusterList); }, + selectDirectoryForDesktopSession(args: { + desktopUri: string; + login: string; + }) { + return ipcRenderer.invoke( + MainProcessIpc.SelectDirectoryForDesktopSession, + args + ); + }, }; } diff --git a/web/packages/teleterm/src/mainProcess/types.ts b/web/packages/teleterm/src/mainProcess/types.ts index 0cfd3905f3fc1..fda7d9c0bcba1 100644 --- a/web/packages/teleterm/src/mainProcess/types.ts +++ b/web/packages/teleterm/src/mainProcess/types.ts @@ -195,6 +195,17 @@ export type MainProcessClient = { */ signalUserInterfaceReadiness(args: { success: boolean }): void; refreshClusterList(): void; + /** + * Opens the Electron directory picker and sends the selected path to tshd through AttachDirectoryToDesktopSession. + * tshd then verifies whether there is an active session for the specified desktop user and attempts to open the directory. + * Once that's done, everything is ready on the tsh daemon to intercept and handle the file system events. + * + * Returns selected directory name. + */ + selectDirectoryForDesktopSession(args: { + desktopUri: string; + login: string; + }): Promise; }; export type ChildProcessAddresses = { @@ -310,6 +321,7 @@ export enum MainProcessIpc { VerifyConnectMyComputerAgent = 'main-process-connect-my-computer-verify-agent', SaveTextToFile = 'main-process-save-text-to-file', ForceFocusWindow = 'main-process-force-focus-window', + SelectDirectoryForDesktopSession = 'main-process-select-directory-for-desktop-session', } export enum WindowsManagerIpc { diff --git a/web/packages/teleterm/src/preload.ts b/web/packages/teleterm/src/preload.ts index 95cb082291988..c1cb1266d4190 100644 --- a/web/packages/teleterm/src/preload.ts +++ b/web/packages/teleterm/src/preload.ts @@ -34,7 +34,11 @@ import { } from 'teleterm/services/grpcCredentials'; import { createFileLoggerService } from 'teleterm/services/logger'; import { createPtyService } from 'teleterm/services/pty/ptyService'; -import { createTshdClient, createVnetClient } from 'teleterm/services/tshd'; +import { + createTshdClient, + createVnetClient, + TshdClient, +} from 'teleterm/services/tshd'; import { loggingInterceptor } from 'teleterm/services/tshd/interceptors'; import { createTshdEventsServer } from 'teleterm/services/tshdEvents'; import { ElectronGlobals, RuntimeSettings } from 'teleterm/types'; @@ -67,7 +71,7 @@ async function getElectronGlobals(): Promise { channelCredentials: credentials.tshd, interceptors: [loggingInterceptor(new Logger('tshd'))], }); - const tshClient = createTshdClient(tshdTransport); + const tshClient = withoutInsecureTshdMethods(createTshdClient(tshdTransport)); const vnetClient = createVnetClient(tshdTransport); const ptyServiceClient = createPtyService( addresses.shared, @@ -164,3 +168,21 @@ async function withErrorLogging( throw e; } } + +/** + * Returns a copy of `TshdClient` with insecure methods disabled + * to prevent access from the untrusted renderer process. + * + * This is possible thanks to the context bridge and the `cloneClient` function, + * which selectively clones only RPC methods and nothing more. + * As a result, disabled methods are inaccessible via the `tshdClient` prototype. + */ +function withoutInsecureTshdMethods(client: TshdClient): TshdClient { + return { + ...client, + attachDirectoryToDesktopSession: () => { + // Prevent the renderer process from sharing directories at arbitrary paths. + throw new Error('This method is not permitted in the renderer process.'); + }, + }; +} diff --git a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts index a3a2443e8f6e0..fbfdc7e7ec1b8 100644 --- a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts +++ b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts @@ -116,6 +116,7 @@ export class MockTshClient implements TshdClient { startHeadlessWatcher = () => new MockedUnaryCall({}); getApp = () => new MockedUnaryCall({ app: makeApp() }); connectToDesktop = undefined; + attachDirectoryToDesktopSession = () => new MockedUnaryCall({}); } export class MockVnetClient implements VnetClient { diff --git a/web/packages/teleterm/src/ui/DocumentDesktopSession/DocumentDesktopSession.tsx b/web/packages/teleterm/src/ui/DocumentDesktopSession/DocumentDesktopSession.tsx index 7bc5bd4b9865a..15f328837be3a 100644 --- a/web/packages/teleterm/src/ui/DocumentDesktopSession/DocumentDesktopSession.tsx +++ b/web/packages/teleterm/src/ui/DocumentDesktopSession/DocumentDesktopSession.tsx @@ -25,10 +25,11 @@ import { makeProcessingAttempt, makeSuccessAttempt, } from 'shared/hooks/useAsync'; -import { BrowserFileSystem, TdpClient } from 'shared/libs/tdp'; +import { SharedDirectoryAccess, TdpClient } from 'shared/libs/tdp'; import { TdpTransport } from 'shared/libs/tdp/client'; import Logger from 'teleterm/logger'; +import { MainProcessClient } from 'teleterm/mainProcess/types'; import { cloneAbortSignal, TshdClient } from 'teleterm/services/tshd'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import Document from 'teleterm/ui/Document'; @@ -62,25 +63,28 @@ export function DocumentDesktopSession(props: { const [client] = useState( () => - new TdpClient(async abortSignal => { - const stream = appCtx.tshd.connectToDesktop({ - abort: cloneAbortSignal(abortSignal), - }); - appCtx.usageService.captureProtocolUse({ - uri: desktopUri, - protocol: 'desktop', - origin, - accessThrough: 'proxy_service', - }); - return adaptGRPCStreamToTdpTransport( - stream, - { - desktopUri, - login, - }, - logger - ); - }, new BrowserFileSystem()) + new TdpClient( + async abortSignal => { + const stream = appCtx.tshd.connectToDesktop({ + abort: cloneAbortSignal(abortSignal), + }); + appCtx.usageService.captureProtocolUse({ + uri: desktopUri, + protocol: 'desktop', + origin, + accessThrough: 'proxy_service', + }); + return adaptGRPCStreamToTdpTransport( + stream, + { desktopUri, login }, + logger + ); + }, + makeTshdFileSystem(appCtx.mainProcessClient, { + desktopUri, + login, + }) + ) ); return ( @@ -135,3 +139,60 @@ async function adaptGRPCStreamToTdpTransport( }, }; } + +/** + * The tshd daemon is responsible for handling directory sharing. + * + * The process begins when the Electron main process opens a directory picker. + * Once a path is selected, it is passed to tshd via the AttachDirectoryToDesktopSession API. + * + * tshd then verifies whether there is an active session for the specified desktop user and attempts to open the directory. + * Once that's done, everything is ready on the tsh daemon to intercept and handle the file system events. + * + * The final step is to send a SharedDirectoryAnnounce message to the server, which is done in the JS TDP client. + * This message is safe to send from the renderer because it only provides + * a display name for the mounted drive on the remote machine and has no effect on local file system operations. + */ +function makeTshdFileSystem( + mainProcessClient: MainProcessClient, + target: { + desktopUri: string; + login: string; + } +): SharedDirectoryAccess { + let directoryName = ''; + return { + selectDirectory: async () => { + directoryName = + await mainProcessClient.selectDirectoryForDesktopSession(target); + }, + getDirectoryName: () => directoryName, + stat: () => { + throw new NotImplemented(); + }, + readDir: () => { + throw new NotImplemented(); + }, + read: () => { + throw new NotImplemented(); + }, + write: () => { + throw new NotImplemented(); + }, + truncate: () => { + throw new NotImplemented(); + }, + create: () => { + throw new NotImplemented(); + }, + delete: () => { + throw new NotImplemented(); + }, + }; +} + +class NotImplemented extends Error { + constructor() { + super('Not implemented, file system operation are handled by tsh demon.'); + } +}