diff --git a/gen/proto/go/teleport/lib/teleterm/v1/app.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/app.pb.go index 1f0eef3891b2a..5c981aa3bd1d4 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/app.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/app.pb.go @@ -92,9 +92,11 @@ type App struct { // proxy hostname] if public_addr is not present. // If the app belongs to a leaf cluster, fqdn is equal to [name].[root cluster proxy hostname]. // - // fqdn is not present for SAML applications. + // fqdn is not present for SAML applications. Available only when the app was fetched through the + // ListUnifiedResources RPC. Fqdn string `protobuf:"bytes,10,opt,name=fqdn,proto3" json:"fqdn,omitempty"` - // aws_roles is a list of AWS IAM roles for the application representing AWS console. + // aws_roles is a list of AWS IAM roles for the application representing AWS console. Available + // only when the app wast fetched through the ListUnifiedResources RPC. AwsRoles []*AWSRole `protobuf:"bytes,11,rep,name=aws_roles,json=awsRoles,proto3" json:"aws_roles,omitempty"` // TCPPorts is a list of ports and port ranges that an app agent can forward connections to. // Only applicable to TCP App Access. diff --git a/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go index 7f5f4f8332bd0..fcfb0d077a532 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go @@ -62,10 +62,11 @@ type Gateway struct { LocalAddress string `protobuf:"bytes,5,opt,name=local_address,json=localAddress,proto3" json:"local_address,omitempty"` // local_port is the gateway address on localhost LocalPort string `protobuf:"bytes,6,opt,name=local_port,json=localPort,proto3" json:"local_port,omitempty"` - // protocol is the gateway protocol + // protocol is the protocol used by the gateway. For databases, it matches the type of the + // database that the gateway targets. For apps, it's either "HTTP" or "TCP". Protocol string `protobuf:"bytes,7,opt,name=protocol,proto3" json:"protocol,omitempty"` // target_subresource_name points at a subresource of the remote resource, for example a - // database name on a database server. + // database name on a database server or a target port of a multi-port TCP app. TargetSubresourceName string `protobuf:"bytes,9,opt,name=target_subresource_name,json=targetSubresourceName,proto3" json:"target_subresource_name,omitempty"` // gateway_cli_client represents a command that the user can execute to connect to the resource // through the gateway. 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 decb858b1d2ef..6f51534ebec7c 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go @@ -4035,6 +4035,96 @@ func (x *AuthenticateWebDeviceResponse) GetConfirmationToken() *v12.DeviceConfir return nil } +type GetAppRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AppUri string `protobuf:"bytes,1,opt,name=app_uri,json=appUri,proto3" json:"app_uri,omitempty"` +} + +func (x *GetAppRequest) Reset() { + *x = GetAppRequest{} + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAppRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAppRequest) ProtoMessage() {} + +func (x *GetAppRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[70] + 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 GetAppRequest.ProtoReflect.Descriptor instead. +func (*GetAppRequest) Descriptor() ([]byte, []int) { + return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{70} +} + +func (x *GetAppRequest) GetAppUri() string { + if x != nil { + return x.AppUri + } + return "" +} + +type GetAppResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + App *App `protobuf:"bytes,1,opt,name=app,proto3" json:"app,omitempty"` +} + +func (x *GetAppResponse) Reset() { + *x = GetAppResponse{} + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAppResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAppResponse) ProtoMessage() {} + +func (x *GetAppResponse) ProtoReflect() protoreflect.Message { + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[71] + 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 GetAppResponse.ProtoReflect.Descriptor instead. +func (*GetAppResponse) Descriptor() ([]byte, []int) { + return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{71} +} + +func (x *GetAppResponse) GetApp() *App { + if x != nil { + return x.App + } + return nil +} + // LoginPasswordlessRequestInit contains fields needed to init the stream request. type LoginPasswordlessRequest_LoginPasswordlessRequestInit struct { state protoimpl.MessageState @@ -4047,7 +4137,7 @@ type LoginPasswordlessRequest_LoginPasswordlessRequestInit struct { func (x *LoginPasswordlessRequest_LoginPasswordlessRequestInit) Reset() { *x = LoginPasswordlessRequest_LoginPasswordlessRequestInit{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[70] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4059,7 +4149,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[70] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4094,7 +4184,7 @@ type LoginPasswordlessRequest_LoginPasswordlessPINResponse struct { func (x *LoginPasswordlessRequest_LoginPasswordlessPINResponse) Reset() { *x = LoginPasswordlessRequest_LoginPasswordlessPINResponse{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[71] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4106,7 +4196,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[71] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4143,7 +4233,7 @@ type LoginPasswordlessRequest_LoginPasswordlessCredentialResponse struct { func (x *LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) Reset() { *x = LoginPasswordlessRequest_LoginPasswordlessCredentialResponse{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[72] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4155,7 +4245,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[72] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4194,7 +4284,7 @@ type LoginRequest_LocalParams struct { func (x *LoginRequest_LocalParams) Reset() { *x = LoginRequest_LocalParams{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[73] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4206,7 +4296,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[73] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4257,7 +4347,7 @@ type LoginRequest_SsoParams struct { func (x *LoginRequest_SsoParams) Reset() { *x = LoginRequest_SsoParams{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[74] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4269,7 +4359,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[74] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4897,365 +4987,377 @@ var file_teleport_lib_teleterm_v1_service_proto_rawDesc = []byte{ 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x2a, 0x97, 0x01, 0x0a, 0x12, 0x50, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x50, 0x72, 0x6f, 0x6d, 0x70, - 0x74, 0x12, 0x23, 0x0a, 0x1f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, 0x45, 0x53, - 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, - 0x52, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x50, 0x49, - 0x4e, 0x10, 0x01, 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, - 0x45, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x54, 0x41, 0x50, 0x10, 0x02, - 0x12, 0x22, 0x0a, 0x1e, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, 0x45, 0x53, 0x53, - 0x5f, 0x50, 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x44, 0x45, 0x4e, 0x54, 0x49, - 0x41, 0x4c, 0x10, 0x03, 0x2a, 0x8a, 0x01, 0x0a, 0x15, 0x46, 0x69, 0x6c, 0x65, 0x54, 0x72, 0x61, - 0x6e, 0x73, 0x66, 0x65, 0x72, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, - 0x0a, 0x23, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, - 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x46, 0x49, 0x4c, 0x45, 0x5f, - 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, - 0x4f, 0x4e, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x01, 0x12, 0x22, 0x0a, - 0x1e, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, - 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x50, 0x4c, 0x4f, 0x41, 0x44, 0x10, - 0x02, 0x2a, 0xcd, 0x01, 0x0a, 0x1b, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x41, 0x75, - 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x2d, 0x0a, 0x29, 0x48, 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, 0x55, - 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, - 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x29, 0x0a, 0x25, 0x48, 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, 0x55, 0x54, - 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, - 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x28, 0x0a, 0x24, 0x48, - 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, - 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x4e, - 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x2a, 0x0a, 0x26, 0x48, 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, - 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x50, 0x50, 0x52, 0x4f, 0x56, 0x45, 0x44, 0x10, - 0x03, 0x32, 0x81, 0x28, 0x0a, 0x0f, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xa0, 0x01, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x54, 0x73, 0x68, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x73, 0x68, 0x64, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3f, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x73, 0x68, 0x64, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x71, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x6f, 0x6f, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x28, 0x0a, 0x0d, 0x47, 0x65, + 0x74, 0x41, 0x70, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x61, + 0x70, 0x70, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x70, + 0x70, 0x55, 0x72, 0x69, 0x22, 0x41, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x03, 0x61, 0x70, 0x70, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, + 0x70, 0x70, 0x52, 0x03, 0x61, 0x70, 0x70, 0x2a, 0x97, 0x01, 0x0a, 0x12, 0x50, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x23, + 0x0a, 0x1f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x50, + 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, + 0x45, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x50, 0x49, 0x4e, 0x10, 0x01, + 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, 0x45, 0x53, 0x53, + 0x5f, 0x50, 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x54, 0x41, 0x50, 0x10, 0x02, 0x12, 0x22, 0x0a, + 0x1e, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x52, + 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x41, 0x4c, 0x10, + 0x03, 0x2a, 0x8a, 0x01, 0x0a, 0x15, 0x46, 0x69, 0x6c, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, + 0x65, 0x72, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x23, 0x46, + 0x49, 0x4c, 0x45, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, 0x49, 0x52, + 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, + 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x54, 0x52, 0x41, + 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, + 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x01, 0x12, 0x22, 0x0a, 0x1e, 0x46, 0x49, + 0x4c, 0x45, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, 0x49, 0x52, 0x45, + 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x50, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x02, 0x2a, 0xcd, + 0x01, 0x0a, 0x1b, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, + 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2d, + 0x0a, 0x29, 0x48, 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x45, + 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x29, 0x0a, + 0x25, 0x48, 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, + 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, + 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x28, 0x0a, 0x24, 0x48, 0x45, 0x41, 0x44, + 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x4e, 0x49, 0x45, 0x44, + 0x10, 0x02, 0x12, 0x2a, 0x0a, 0x26, 0x48, 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, + 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, + 0x41, 0x54, 0x45, 0x5f, 0x41, 0x50, 0x50, 0x52, 0x4f, 0x56, 0x45, 0x44, 0x10, 0x03, 0x32, 0xde, + 0x28, 0x0a, 0x0f, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0xa0, 0x01, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x73, 0x68, + 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x12, 0x3e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x73, 0x68, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3f, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x73, 0x68, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x71, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x6f, + 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, + 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, + 0x4c, 0x65, 0x61, 0x66, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x75, 0x73, - 0x74, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, - 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x10, 0x4c, - 0x69, 0x73, 0x74, 0x4c, 0x65, 0x61, 0x66, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x12, - 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, - 0x65, 0x61, 0x66, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, - 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x14, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x61, 0x64, - 0x6c, 0x65, 0x73, 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x12, 0x35, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x61, 0x64, - 0x6c, 0x65, 0x73, 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, - 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, - 0x61, 0x72, 0x74, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, - 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x11, 0x4c, 0x69, - 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, - 0x32, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, - 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, - 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x65, 0x61, 0x66, + 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x85, 0x01, 0x0a, 0x14, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, + 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x12, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, + 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, + 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x44, + 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x32, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, + 0x62, 0x61, 0x73, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x73, 0x12, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x7c, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x32, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x79, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, + 0x88, 0x02, 0x01, 0x12, 0x7c, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x32, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x74, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, + 0x75, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, - 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x13, 0x52, - 0x65, 0x76, 0x69, 0x65, 0x77, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, - 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x76, 0x69, 0x65, 0x77, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x82, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x61, 0x62, - 0x6c, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x61, 0x62, 0x6c, - 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0a, 0x41, 0x73, 0x73, 0x75, 0x6d, 0x65, 0x52, 0x6f, - 0x6c, 0x65, 0x12, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, - 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, - 0x73, 0x75, 0x6d, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x14, 0x50, 0x72, 0x6f, - 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, - 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, - 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x79, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, - 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x8e, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, - 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x12, 0x38, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, - 0x73, 0x74, 0x65, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, 0x64, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x8e, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, - 0x65, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x38, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x75, 0x62, - 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, - 0x73, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, - 0x12, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x43, - 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, - 0x12, 0x68, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, - 0x72, 0x12, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, - 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, - 0x6f, 0x76, 0x65, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, - 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x0c, 0x4c, 0x69, - 0x73, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x12, 0x2d, 0x2e, 0x74, 0x65, 0x6c, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x74, 0x0a, 0x13, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, + 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x13, 0x52, 0x65, 0x76, 0x69, + 0x65, 0x77, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, + 0x77, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, + 0x13, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, + 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, + 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, - 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x62, 0x0a, 0x0a, 0x41, 0x73, 0x73, 0x75, 0x6d, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x12, + 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x75, 0x6d, + 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x6d, 0x6f, 0x74, + 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x6f, 0x74, + 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, + 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8e, 0x01, + 0x0a, 0x17, 0x47, 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, 0x64, 0x41, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x12, 0x38, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0d, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x2e, 0x2e, 0x74, 0x65, 0x6c, + 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, + 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8e, + 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, + 0x73, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x38, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, + 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x5c, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x2b, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x43, 0x6c, 0x75, 0x73, + 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x68, 0x0a, - 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x2e, + 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x68, 0x0a, + 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, - 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, + 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x86, 0x01, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x47, - 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x75, 0x62, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, - 0x79, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x12, 0x6e, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x6f, - 0x63, 0x61, 0x6c, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x47, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x12, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x6f, 0x63, - 0x61, 0x6c, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x12, 0x6b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x73, 0x12, 0x30, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, - 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, - 0x2e, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x5c, 0x0a, - 0x0a, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x2b, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, - 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x58, 0x0a, 0x05, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x26, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x80, 0x01, 0x0a, 0x11, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x12, 0x32, 0x2e, 0x74, 0x65, + 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x68, 0x0a, 0x0d, 0x52, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, - 0x75, 0x74, 0x12, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, - 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, - 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, + 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x47, 0x61, 0x74, + 0x65, 0x77, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x0c, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, - 0x46, 0x69, 0x6c, 0x65, 0x12, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x86, 0x01, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x53, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x6e, 0x0a, + 0x13, 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x6f, 0x63, 0x61, 0x6c, + 0x50, 0x6f, 0x72, 0x74, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, - 0x46, 0x69, 0x6c, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, - 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x46, - 0x69, 0x6c, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x67, 0x72, - 0x65, 0x73, 0x73, 0x30, 0x01, 0x12, 0x6e, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x55, - 0x73, 0x61, 0x67, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, + 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x6b, 0x0a, + 0x0f, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x12, 0x30, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, + 0x75, 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, + 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, + 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x5c, 0x0a, 0x0a, 0x47, 0x65, + 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x58, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x12, 0x26, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, + 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x80, 0x01, 0x0a, 0x11, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x12, 0x32, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x6c, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xac, 0x01, 0x0a, 0x21, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x42, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x65, 0x61, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, + 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x6f, 0x0a, 0x0c, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x46, 0x69, 0x6c, + 0x65, 0x12, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6c, + 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6c, 0x65, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, + 0x30, 0x01, 0x12, 0x6e, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, + 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0xac, 0x01, 0x0a, 0x21, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x43, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9a, 0x01, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, - 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x3c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, - 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, - 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, - 0x70, 0x75, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0xa9, 0x01, 0x0a, 0x20, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, - 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x41, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x42, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, + 0x73, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x43, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x65, + 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x9a, 0x01, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6c, + 0x65, 0x12, 0x3c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, + 0x75, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, + 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa9, + 0x01, 0x0a, 0x20, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x41, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, + 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x42, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x42, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa9, 0x01, - 0x0a, 0x20, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4a, 0x6f, - 0x69, 0x6e, 0x12, 0x41, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, + 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa9, 0x01, 0x0a, 0x20, 0x57, + 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, + 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x12, + 0x41, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x46, + 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, + 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x42, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x42, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, - 0x2e, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, - 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4a, 0x6f, 0x69, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9a, 0x01, 0x0a, 0x1b, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, - 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x3c, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9a, 0x01, 0x0a, 0x1b, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, + 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x3c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, + 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, + 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, + 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x9d, 0x01, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9d, 0x01, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, - 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, - 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, + 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x3e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, + 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x6e, 0x69, 0x66, + 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x35, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x6e, 0x69, 0x66, + 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x55, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7f, 0x0a, 0x12, 0x47, + 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, + 0x73, 0x12, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, - 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x55, - 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, - 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, - 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, + 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, + 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7f, - 0x0a, 0x12, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, - 0x6e, 0x63, 0x65, 0x73, 0x12, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, - 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, - 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, - 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x88, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, - 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, + 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, + 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, - 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x37, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, - 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x15, 0x41, - 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, - 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x74, + 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x57, 0x65, 0x62, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x12, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x54, 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x2f, 0x6c, 0x69, 0x62, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2f, 0x76, 0x31, - 0x3b, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x54, 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, + 0x61, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, + 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x6c, 0x69, 0x62, 0x2f, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2f, 0x76, 0x31, 0x3b, 0x74, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x72, 0x6d, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -5271,7 +5373,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, 75) +var file_teleport_lib_teleterm_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 77) 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 @@ -5346,154 +5448,159 @@ var file_teleport_lib_teleterm_v1_service_proto_goTypes = []any{ (*UserPreferences)(nil), // 70: teleport.lib.teleterm.v1.UserPreferences (*AuthenticateWebDeviceRequest)(nil), // 71: teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest (*AuthenticateWebDeviceResponse)(nil), // 72: teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse - (*LoginPasswordlessRequest_LoginPasswordlessRequestInit)(nil), // 73: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit - (*LoginPasswordlessRequest_LoginPasswordlessPINResponse)(nil), // 74: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse - (*LoginPasswordlessRequest_LoginPasswordlessCredentialResponse)(nil), // 75: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse - (*LoginRequest_LocalParams)(nil), // 76: teleport.lib.teleterm.v1.LoginRequest.LocalParams - (*LoginRequest_SsoParams)(nil), // 77: teleport.lib.teleterm.v1.LoginRequest.SsoParams - (*AccessRequest)(nil), // 78: teleport.lib.teleterm.v1.AccessRequest - (*ResourceID)(nil), // 79: teleport.lib.teleterm.v1.ResourceID - (*timestamppb.Timestamp)(nil), // 80: google.protobuf.Timestamp - (*v1.AccessList)(nil), // 81: teleport.accesslist.v1.AccessList - (*KubeResource)(nil), // 82: teleport.lib.teleterm.v1.KubeResource - (*Cluster)(nil), // 83: teleport.lib.teleterm.v1.Cluster - (*Gateway)(nil), // 84: teleport.lib.teleterm.v1.Gateway - (*Server)(nil), // 85: teleport.lib.teleterm.v1.Server - (*Database)(nil), // 86: teleport.lib.teleterm.v1.Database - (*Kube)(nil), // 87: teleport.lib.teleterm.v1.Kube - (*App)(nil), // 88: teleport.lib.teleterm.v1.App - (*v11.ClusterUserPreferences)(nil), // 89: teleport.userpreferences.v1.ClusterUserPreferences - (*v11.UnifiedResourcePreferences)(nil), // 90: teleport.userpreferences.v1.UnifiedResourcePreferences - (*v12.DeviceWebToken)(nil), // 91: teleport.devicetrust.v1.DeviceWebToken - (*v12.DeviceConfirmationToken)(nil), // 92: teleport.devicetrust.v1.DeviceConfirmationToken - (*ReportUsageEventRequest)(nil), // 93: teleport.lib.teleterm.v1.ReportUsageEventRequest - (*AuthSettings)(nil), // 94: teleport.lib.teleterm.v1.AuthSettings + (*GetAppRequest)(nil), // 73: teleport.lib.teleterm.v1.GetAppRequest + (*GetAppResponse)(nil), // 74: teleport.lib.teleterm.v1.GetAppResponse + (*LoginPasswordlessRequest_LoginPasswordlessRequestInit)(nil), // 75: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit + (*LoginPasswordlessRequest_LoginPasswordlessPINResponse)(nil), // 76: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse + (*LoginPasswordlessRequest_LoginPasswordlessCredentialResponse)(nil), // 77: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse + (*LoginRequest_LocalParams)(nil), // 78: teleport.lib.teleterm.v1.LoginRequest.LocalParams + (*LoginRequest_SsoParams)(nil), // 79: teleport.lib.teleterm.v1.LoginRequest.SsoParams + (*AccessRequest)(nil), // 80: teleport.lib.teleterm.v1.AccessRequest + (*ResourceID)(nil), // 81: teleport.lib.teleterm.v1.ResourceID + (*timestamppb.Timestamp)(nil), // 82: google.protobuf.Timestamp + (*v1.AccessList)(nil), // 83: teleport.accesslist.v1.AccessList + (*KubeResource)(nil), // 84: teleport.lib.teleterm.v1.KubeResource + (*Cluster)(nil), // 85: teleport.lib.teleterm.v1.Cluster + (*Gateway)(nil), // 86: teleport.lib.teleterm.v1.Gateway + (*Server)(nil), // 87: teleport.lib.teleterm.v1.Server + (*Database)(nil), // 88: teleport.lib.teleterm.v1.Database + (*Kube)(nil), // 89: teleport.lib.teleterm.v1.Kube + (*App)(nil), // 90: teleport.lib.teleterm.v1.App + (*v11.ClusterUserPreferences)(nil), // 91: teleport.userpreferences.v1.ClusterUserPreferences + (*v11.UnifiedResourcePreferences)(nil), // 92: teleport.userpreferences.v1.UnifiedResourcePreferences + (*v12.DeviceWebToken)(nil), // 93: teleport.devicetrust.v1.DeviceWebToken + (*v12.DeviceConfirmationToken)(nil), // 94: teleport.devicetrust.v1.DeviceConfirmationToken + (*ReportUsageEventRequest)(nil), // 95: teleport.lib.teleterm.v1.ReportUsageEventRequest + (*AuthSettings)(nil), // 96: teleport.lib.teleterm.v1.AuthSettings } var file_teleport_lib_teleterm_v1_service_proto_depIdxs = []int32{ - 78, // 0: teleport.lib.teleterm.v1.GetAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest - 78, // 1: teleport.lib.teleterm.v1.GetAccessRequestsResponse.requests:type_name -> teleport.lib.teleterm.v1.AccessRequest - 79, // 2: teleport.lib.teleterm.v1.CreateAccessRequestRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID - 80, // 3: teleport.lib.teleterm.v1.CreateAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp - 80, // 4: teleport.lib.teleterm.v1.CreateAccessRequestRequest.max_duration:type_name -> google.protobuf.Timestamp - 80, // 5: teleport.lib.teleterm.v1.CreateAccessRequestRequest.request_ttl:type_name -> google.protobuf.Timestamp - 78, // 6: teleport.lib.teleterm.v1.CreateAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest - 79, // 7: teleport.lib.teleterm.v1.GetRequestableRolesRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID - 80, // 8: teleport.lib.teleterm.v1.ReviewAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp - 78, // 9: teleport.lib.teleterm.v1.ReviewAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest - 78, // 10: teleport.lib.teleterm.v1.PromoteAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest - 81, // 11: teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse.access_lists:type_name -> teleport.accesslist.v1.AccessList - 82, // 12: teleport.lib.teleterm.v1.ListKubernetesResourcesResponse.resources:type_name -> teleport.lib.teleterm.v1.KubeResource + 80, // 0: teleport.lib.teleterm.v1.GetAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest + 80, // 1: teleport.lib.teleterm.v1.GetAccessRequestsResponse.requests:type_name -> teleport.lib.teleterm.v1.AccessRequest + 81, // 2: teleport.lib.teleterm.v1.CreateAccessRequestRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID + 82, // 3: teleport.lib.teleterm.v1.CreateAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp + 82, // 4: teleport.lib.teleterm.v1.CreateAccessRequestRequest.max_duration:type_name -> google.protobuf.Timestamp + 82, // 5: teleport.lib.teleterm.v1.CreateAccessRequestRequest.request_ttl:type_name -> google.protobuf.Timestamp + 80, // 6: teleport.lib.teleterm.v1.CreateAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest + 81, // 7: teleport.lib.teleterm.v1.GetRequestableRolesRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID + 82, // 8: teleport.lib.teleterm.v1.ReviewAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp + 80, // 9: teleport.lib.teleterm.v1.ReviewAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest + 80, // 10: teleport.lib.teleterm.v1.PromoteAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest + 83, // 11: teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse.access_lists:type_name -> teleport.accesslist.v1.AccessList + 84, // 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 - 73, // 15: teleport.lib.teleterm.v1.LoginPasswordlessRequest.init:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit - 74, // 16: teleport.lib.teleterm.v1.LoginPasswordlessRequest.pin:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse - 75, // 17: teleport.lib.teleterm.v1.LoginPasswordlessRequest.credential:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse + 75, // 15: teleport.lib.teleterm.v1.LoginPasswordlessRequest.init:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit + 76, // 16: teleport.lib.teleterm.v1.LoginPasswordlessRequest.pin:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse + 77, // 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 - 76, // 19: teleport.lib.teleterm.v1.LoginRequest.local:type_name -> teleport.lib.teleterm.v1.LoginRequest.LocalParams - 77, // 20: teleport.lib.teleterm.v1.LoginRequest.sso:type_name -> teleport.lib.teleterm.v1.LoginRequest.SsoParams - 83, // 21: teleport.lib.teleterm.v1.ListClustersResponse.clusters:type_name -> teleport.lib.teleterm.v1.Cluster - 84, // 22: teleport.lib.teleterm.v1.ListGatewaysResponse.gateways:type_name -> teleport.lib.teleterm.v1.Gateway - 85, // 23: teleport.lib.teleterm.v1.GetServersResponse.agents:type_name -> teleport.lib.teleterm.v1.Server + 78, // 19: teleport.lib.teleterm.v1.LoginRequest.local:type_name -> teleport.lib.teleterm.v1.LoginRequest.LocalParams + 79, // 20: teleport.lib.teleterm.v1.LoginRequest.sso:type_name -> teleport.lib.teleterm.v1.LoginRequest.SsoParams + 85, // 21: teleport.lib.teleterm.v1.ListClustersResponse.clusters:type_name -> teleport.lib.teleterm.v1.Cluster + 86, // 22: teleport.lib.teleterm.v1.ListGatewaysResponse.gateways:type_name -> teleport.lib.teleterm.v1.Gateway + 87, // 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 - 85, // 25: teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse.server:type_name -> teleport.lib.teleterm.v1.Server + 87, // 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 - 86, // 28: teleport.lib.teleterm.v1.PaginatedResource.database:type_name -> teleport.lib.teleterm.v1.Database - 85, // 29: teleport.lib.teleterm.v1.PaginatedResource.server:type_name -> teleport.lib.teleterm.v1.Server - 87, // 30: teleport.lib.teleterm.v1.PaginatedResource.kube:type_name -> teleport.lib.teleterm.v1.Kube - 88, // 31: teleport.lib.teleterm.v1.PaginatedResource.app:type_name -> teleport.lib.teleterm.v1.App + 88, // 28: teleport.lib.teleterm.v1.PaginatedResource.database:type_name -> teleport.lib.teleterm.v1.Database + 87, // 29: teleport.lib.teleterm.v1.PaginatedResource.server:type_name -> teleport.lib.teleterm.v1.Server + 89, // 30: teleport.lib.teleterm.v1.PaginatedResource.kube:type_name -> teleport.lib.teleterm.v1.Kube + 90, // 31: teleport.lib.teleterm.v1.PaginatedResource.app:type_name -> teleport.lib.teleterm.v1.App 70, // 32: teleport.lib.teleterm.v1.GetUserPreferencesResponse.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences 70, // 33: teleport.lib.teleterm.v1.UpdateUserPreferencesRequest.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences 70, // 34: teleport.lib.teleterm.v1.UpdateUserPreferencesResponse.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences - 89, // 35: teleport.lib.teleterm.v1.UserPreferences.cluster_preferences:type_name -> teleport.userpreferences.v1.ClusterUserPreferences - 90, // 36: teleport.lib.teleterm.v1.UserPreferences.unified_resource_preferences:type_name -> teleport.userpreferences.v1.UnifiedResourcePreferences - 91, // 37: teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest.device_web_token:type_name -> teleport.devicetrust.v1.DeviceWebToken - 92, // 38: teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse.confirmation_token:type_name -> teleport.devicetrust.v1.DeviceConfirmationToken - 48, // 39: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:input_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressRequest - 34, // 40: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:input_type -> teleport.lib.teleterm.v1.ListClustersRequest - 36, // 41: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:input_type -> teleport.lib.teleterm.v1.ListLeafClustersRequest - 7, // 42: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:input_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherRequest - 37, // 43: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:input_type -> teleport.lib.teleterm.v1.ListDatabaseUsersRequest - 45, // 44: teleport.lib.teleterm.v1.TerminalService.GetServers:input_type -> teleport.lib.teleterm.v1.GetServersRequest - 10, // 45: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:input_type -> teleport.lib.teleterm.v1.GetAccessRequestsRequest - 9, // 46: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:input_type -> teleport.lib.teleterm.v1.GetAccessRequestRequest - 13, // 47: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:input_type -> teleport.lib.teleterm.v1.DeleteAccessRequestRequest - 14, // 48: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:input_type -> teleport.lib.teleterm.v1.CreateAccessRequestRequest - 19, // 49: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:input_type -> teleport.lib.teleterm.v1.ReviewAccessRequestRequest - 17, // 50: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:input_type -> teleport.lib.teleterm.v1.GetRequestableRolesRequest - 16, // 51: teleport.lib.teleterm.v1.TerminalService.AssumeRole:input_type -> teleport.lib.teleterm.v1.AssumeRoleRequest - 21, // 52: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:input_type -> teleport.lib.teleterm.v1.PromoteAccessRequestRequest - 23, // 53: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:input_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsRequest - 25, // 54: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:input_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesRequest - 33, // 55: teleport.lib.teleterm.v1.TerminalService.AddCluster:input_type -> teleport.lib.teleterm.v1.AddClusterRequest - 4, // 56: teleport.lib.teleterm.v1.TerminalService.RemoveCluster:input_type -> teleport.lib.teleterm.v1.RemoveClusterRequest - 40, // 57: teleport.lib.teleterm.v1.TerminalService.ListGateways:input_type -> teleport.lib.teleterm.v1.ListGatewaysRequest - 39, // 58: teleport.lib.teleterm.v1.TerminalService.CreateGateway:input_type -> teleport.lib.teleterm.v1.CreateGatewayRequest - 42, // 59: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:input_type -> teleport.lib.teleterm.v1.RemoveGatewayRequest - 43, // 60: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:input_type -> teleport.lib.teleterm.v1.SetGatewayTargetSubresourceNameRequest - 44, // 61: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:input_type -> teleport.lib.teleterm.v1.SetGatewayLocalPortRequest - 47, // 62: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:input_type -> teleport.lib.teleterm.v1.GetAuthSettingsRequest - 5, // 63: teleport.lib.teleterm.v1.TerminalService.GetCluster:input_type -> teleport.lib.teleterm.v1.GetClusterRequest - 32, // 64: teleport.lib.teleterm.v1.TerminalService.Login:input_type -> teleport.lib.teleterm.v1.LoginRequest - 29, // 65: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:input_type -> teleport.lib.teleterm.v1.LoginPasswordlessRequest - 6, // 66: teleport.lib.teleterm.v1.TerminalService.Logout:input_type -> teleport.lib.teleterm.v1.LogoutRequest - 30, // 67: teleport.lib.teleterm.v1.TerminalService.TransferFile:input_type -> teleport.lib.teleterm.v1.FileTransferRequest - 93, // 68: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:input_type -> teleport.lib.teleterm.v1.ReportUsageEventRequest - 50, // 69: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:input_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest - 52, // 70: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleRequest - 54, // 71: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenRequest - 56, // 72: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:input_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinRequest - 58, // 73: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:input_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeRequest - 60, // 74: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:input_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameRequest - 62, // 75: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:input_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesRequest - 66, // 76: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:input_type -> teleport.lib.teleterm.v1.GetUserPreferencesRequest - 68, // 77: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:input_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesRequest - 71, // 78: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:input_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest - 49, // 79: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:output_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressResponse - 35, // 80: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse - 35, // 81: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse - 8, // 82: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:output_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherResponse - 38, // 83: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:output_type -> teleport.lib.teleterm.v1.ListDatabaseUsersResponse - 46, // 84: teleport.lib.teleterm.v1.TerminalService.GetServers:output_type -> teleport.lib.teleterm.v1.GetServersResponse - 12, // 85: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:output_type -> teleport.lib.teleterm.v1.GetAccessRequestsResponse - 11, // 86: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:output_type -> teleport.lib.teleterm.v1.GetAccessRequestResponse - 3, // 87: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 15, // 88: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:output_type -> teleport.lib.teleterm.v1.CreateAccessRequestResponse - 20, // 89: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:output_type -> teleport.lib.teleterm.v1.ReviewAccessRequestResponse - 18, // 90: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:output_type -> teleport.lib.teleterm.v1.GetRequestableRolesResponse - 3, // 91: teleport.lib.teleterm.v1.TerminalService.AssumeRole:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 22, // 92: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:output_type -> teleport.lib.teleterm.v1.PromoteAccessRequestResponse - 24, // 93: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:output_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse - 26, // 94: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:output_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesResponse - 83, // 95: teleport.lib.teleterm.v1.TerminalService.AddCluster:output_type -> teleport.lib.teleterm.v1.Cluster - 3, // 96: teleport.lib.teleterm.v1.TerminalService.RemoveCluster:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 41, // 97: teleport.lib.teleterm.v1.TerminalService.ListGateways:output_type -> teleport.lib.teleterm.v1.ListGatewaysResponse - 84, // 98: teleport.lib.teleterm.v1.TerminalService.CreateGateway:output_type -> teleport.lib.teleterm.v1.Gateway - 3, // 99: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 84, // 100: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:output_type -> teleport.lib.teleterm.v1.Gateway - 84, // 101: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:output_type -> teleport.lib.teleterm.v1.Gateway - 94, // 102: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:output_type -> teleport.lib.teleterm.v1.AuthSettings - 83, // 103: teleport.lib.teleterm.v1.TerminalService.GetCluster:output_type -> teleport.lib.teleterm.v1.Cluster - 3, // 104: teleport.lib.teleterm.v1.TerminalService.Login:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 28, // 105: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:output_type -> teleport.lib.teleterm.v1.LoginPasswordlessResponse - 3, // 106: teleport.lib.teleterm.v1.TerminalService.Logout:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 31, // 107: teleport.lib.teleterm.v1.TerminalService.TransferFile:output_type -> teleport.lib.teleterm.v1.FileTransferProgress - 3, // 108: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 51, // 109: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:output_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateResponse - 53, // 110: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleResponse - 55, // 111: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenResponse - 57, // 112: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:output_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse - 59, // 113: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:output_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeResponse - 61, // 114: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:output_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameResponse - 64, // 115: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:output_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesResponse - 67, // 116: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:output_type -> teleport.lib.teleterm.v1.GetUserPreferencesResponse - 69, // 117: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:output_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesResponse - 72, // 118: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:output_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse - 79, // [79:119] is the sub-list for method output_type - 39, // [39:79] is the sub-list for method input_type - 39, // [39:39] is the sub-list for extension type_name - 39, // [39:39] is the sub-list for extension extendee - 0, // [0:39] is the sub-list for field type_name + 91, // 35: teleport.lib.teleterm.v1.UserPreferences.cluster_preferences:type_name -> teleport.userpreferences.v1.ClusterUserPreferences + 92, // 36: teleport.lib.teleterm.v1.UserPreferences.unified_resource_preferences:type_name -> teleport.userpreferences.v1.UnifiedResourcePreferences + 93, // 37: teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest.device_web_token:type_name -> teleport.devicetrust.v1.DeviceWebToken + 94, // 38: teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse.confirmation_token:type_name -> teleport.devicetrust.v1.DeviceConfirmationToken + 90, // 39: teleport.lib.teleterm.v1.GetAppResponse.app:type_name -> teleport.lib.teleterm.v1.App + 48, // 40: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:input_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressRequest + 34, // 41: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:input_type -> teleport.lib.teleterm.v1.ListClustersRequest + 36, // 42: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:input_type -> teleport.lib.teleterm.v1.ListLeafClustersRequest + 7, // 43: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:input_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherRequest + 37, // 44: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:input_type -> teleport.lib.teleterm.v1.ListDatabaseUsersRequest + 45, // 45: teleport.lib.teleterm.v1.TerminalService.GetServers:input_type -> teleport.lib.teleterm.v1.GetServersRequest + 10, // 46: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:input_type -> teleport.lib.teleterm.v1.GetAccessRequestsRequest + 9, // 47: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:input_type -> teleport.lib.teleterm.v1.GetAccessRequestRequest + 13, // 48: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:input_type -> teleport.lib.teleterm.v1.DeleteAccessRequestRequest + 14, // 49: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:input_type -> teleport.lib.teleterm.v1.CreateAccessRequestRequest + 19, // 50: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:input_type -> teleport.lib.teleterm.v1.ReviewAccessRequestRequest + 17, // 51: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:input_type -> teleport.lib.teleterm.v1.GetRequestableRolesRequest + 16, // 52: teleport.lib.teleterm.v1.TerminalService.AssumeRole:input_type -> teleport.lib.teleterm.v1.AssumeRoleRequest + 21, // 53: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:input_type -> teleport.lib.teleterm.v1.PromoteAccessRequestRequest + 23, // 54: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:input_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsRequest + 25, // 55: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:input_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesRequest + 33, // 56: teleport.lib.teleterm.v1.TerminalService.AddCluster:input_type -> teleport.lib.teleterm.v1.AddClusterRequest + 4, // 57: teleport.lib.teleterm.v1.TerminalService.RemoveCluster:input_type -> teleport.lib.teleterm.v1.RemoveClusterRequest + 40, // 58: teleport.lib.teleterm.v1.TerminalService.ListGateways:input_type -> teleport.lib.teleterm.v1.ListGatewaysRequest + 39, // 59: teleport.lib.teleterm.v1.TerminalService.CreateGateway:input_type -> teleport.lib.teleterm.v1.CreateGatewayRequest + 42, // 60: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:input_type -> teleport.lib.teleterm.v1.RemoveGatewayRequest + 43, // 61: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:input_type -> teleport.lib.teleterm.v1.SetGatewayTargetSubresourceNameRequest + 44, // 62: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:input_type -> teleport.lib.teleterm.v1.SetGatewayLocalPortRequest + 47, // 63: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:input_type -> teleport.lib.teleterm.v1.GetAuthSettingsRequest + 5, // 64: teleport.lib.teleterm.v1.TerminalService.GetCluster:input_type -> teleport.lib.teleterm.v1.GetClusterRequest + 32, // 65: teleport.lib.teleterm.v1.TerminalService.Login:input_type -> teleport.lib.teleterm.v1.LoginRequest + 29, // 66: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:input_type -> teleport.lib.teleterm.v1.LoginPasswordlessRequest + 6, // 67: teleport.lib.teleterm.v1.TerminalService.Logout:input_type -> teleport.lib.teleterm.v1.LogoutRequest + 30, // 68: teleport.lib.teleterm.v1.TerminalService.TransferFile:input_type -> teleport.lib.teleterm.v1.FileTransferRequest + 95, // 69: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:input_type -> teleport.lib.teleterm.v1.ReportUsageEventRequest + 50, // 70: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:input_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest + 52, // 71: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleRequest + 54, // 72: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenRequest + 56, // 73: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:input_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinRequest + 58, // 74: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:input_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeRequest + 60, // 75: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:input_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameRequest + 62, // 76: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:input_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesRequest + 66, // 77: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:input_type -> teleport.lib.teleterm.v1.GetUserPreferencesRequest + 68, // 78: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:input_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesRequest + 71, // 79: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:input_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest + 73, // 80: teleport.lib.teleterm.v1.TerminalService.GetApp:input_type -> teleport.lib.teleterm.v1.GetAppRequest + 49, // 81: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:output_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressResponse + 35, // 82: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse + 35, // 83: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse + 8, // 84: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:output_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherResponse + 38, // 85: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:output_type -> teleport.lib.teleterm.v1.ListDatabaseUsersResponse + 46, // 86: teleport.lib.teleterm.v1.TerminalService.GetServers:output_type -> teleport.lib.teleterm.v1.GetServersResponse + 12, // 87: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:output_type -> teleport.lib.teleterm.v1.GetAccessRequestsResponse + 11, // 88: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:output_type -> teleport.lib.teleterm.v1.GetAccessRequestResponse + 3, // 89: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 15, // 90: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:output_type -> teleport.lib.teleterm.v1.CreateAccessRequestResponse + 20, // 91: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:output_type -> teleport.lib.teleterm.v1.ReviewAccessRequestResponse + 18, // 92: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:output_type -> teleport.lib.teleterm.v1.GetRequestableRolesResponse + 3, // 93: teleport.lib.teleterm.v1.TerminalService.AssumeRole:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 22, // 94: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:output_type -> teleport.lib.teleterm.v1.PromoteAccessRequestResponse + 24, // 95: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:output_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse + 26, // 96: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:output_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesResponse + 85, // 97: teleport.lib.teleterm.v1.TerminalService.AddCluster:output_type -> teleport.lib.teleterm.v1.Cluster + 3, // 98: teleport.lib.teleterm.v1.TerminalService.RemoveCluster:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 41, // 99: teleport.lib.teleterm.v1.TerminalService.ListGateways:output_type -> teleport.lib.teleterm.v1.ListGatewaysResponse + 86, // 100: teleport.lib.teleterm.v1.TerminalService.CreateGateway:output_type -> teleport.lib.teleterm.v1.Gateway + 3, // 101: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 86, // 102: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:output_type -> teleport.lib.teleterm.v1.Gateway + 86, // 103: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:output_type -> teleport.lib.teleterm.v1.Gateway + 96, // 104: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:output_type -> teleport.lib.teleterm.v1.AuthSettings + 85, // 105: teleport.lib.teleterm.v1.TerminalService.GetCluster:output_type -> teleport.lib.teleterm.v1.Cluster + 3, // 106: teleport.lib.teleterm.v1.TerminalService.Login:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 28, // 107: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:output_type -> teleport.lib.teleterm.v1.LoginPasswordlessResponse + 3, // 108: teleport.lib.teleterm.v1.TerminalService.Logout:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 31, // 109: teleport.lib.teleterm.v1.TerminalService.TransferFile:output_type -> teleport.lib.teleterm.v1.FileTransferProgress + 3, // 110: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 51, // 111: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:output_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateResponse + 53, // 112: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleResponse + 55, // 113: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenResponse + 57, // 114: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:output_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse + 59, // 115: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:output_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeResponse + 61, // 116: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:output_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameResponse + 64, // 117: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:output_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesResponse + 67, // 118: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:output_type -> teleport.lib.teleterm.v1.GetUserPreferencesResponse + 69, // 119: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:output_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesResponse + 72, // 120: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:output_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse + 74, // 121: teleport.lib.teleterm.v1.TerminalService.GetApp:output_type -> teleport.lib.teleterm.v1.GetAppResponse + 81, // [81:122] is the sub-list for method output_type + 40, // [40:81] is the sub-list for method input_type + 40, // [40:40] is the sub-list for extension type_name + 40, // [40:40] is the sub-list for extension extendee + 0, // [0:40] is the sub-list for field type_name } func init() { file_teleport_lib_teleterm_v1_service_proto_init() } @@ -5531,7 +5638,7 @@ func file_teleport_lib_teleterm_v1_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_teleport_lib_teleterm_v1_service_proto_rawDesc, NumEnums: 3, - NumMessages: 75, + NumMessages: 77, 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 da9eec4127ccf..317a9e59b7e3c 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 @@ -76,6 +76,7 @@ const ( TerminalService_GetUserPreferences_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/GetUserPreferences" TerminalService_UpdateUserPreferences_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/UpdateUserPreferences" TerminalService_AuthenticateWebDevice_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/AuthenticateWebDevice" + TerminalService_GetApp_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/GetApp" ) // TerminalServiceClient is the client API for TerminalService service. @@ -213,6 +214,9 @@ type TerminalServiceClient interface { // See // https://github.com/gravitational/teleport.e/blob/master/rfd/0009e-device-trust-web-support.md#device-web-authentication. AuthenticateWebDevice(ctx context.Context, in *AuthenticateWebDeviceRequest, opts ...grpc.CallOption) (*AuthenticateWebDeviceResponse, error) + // GetApp returns details of an app resource. It does not include information about AWS roles and + // FQDN. + GetApp(ctx context.Context, in *GetAppRequest, opts ...grpc.CallOption) (*GetAppResponse, error) } type terminalServiceClient struct { @@ -636,6 +640,16 @@ func (c *terminalServiceClient) AuthenticateWebDevice(ctx context.Context, in *A return out, nil } +func (c *terminalServiceClient) GetApp(ctx context.Context, in *GetAppRequest, opts ...grpc.CallOption) (*GetAppResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetAppResponse) + err := c.cc.Invoke(ctx, TerminalService_GetApp_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. @@ -771,6 +785,9 @@ type TerminalServiceServer interface { // See // https://github.com/gravitational/teleport.e/blob/master/rfd/0009e-device-trust-web-support.md#device-web-authentication. AuthenticateWebDevice(context.Context, *AuthenticateWebDeviceRequest) (*AuthenticateWebDeviceResponse, error) + // GetApp returns details of an app resource. It does not include information about AWS roles and + // FQDN. + GetApp(context.Context, *GetAppRequest) (*GetAppResponse, error) mustEmbedUnimplementedTerminalServiceServer() } @@ -901,6 +918,9 @@ func (UnimplementedTerminalServiceServer) UpdateUserPreferences(context.Context, func (UnimplementedTerminalServiceServer) AuthenticateWebDevice(context.Context, *AuthenticateWebDeviceRequest) (*AuthenticateWebDeviceResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AuthenticateWebDevice not implemented") } +func (UnimplementedTerminalServiceServer) GetApp(context.Context, *GetAppRequest) (*GetAppResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetApp not implemented") +} func (UnimplementedTerminalServiceServer) mustEmbedUnimplementedTerminalServiceServer() {} func (UnimplementedTerminalServiceServer) testEmbeddedByValue() {} @@ -1624,6 +1644,24 @@ func _TerminalService_AuthenticateWebDevice_Handler(srv interface{}, ctx context return interceptor(ctx, in, info, handler) } +func _TerminalService_GetApp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAppRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TerminalServiceServer).GetApp(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TerminalService_GetApp_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TerminalServiceServer).GetApp(ctx, req.(*GetAppRequest)) + } + 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) @@ -1783,6 +1821,10 @@ var TerminalService_ServiceDesc = grpc.ServiceDesc{ MethodName: "AuthenticateWebDevice", Handler: _TerminalService_AuthenticateWebDevice_Handler, }, + { + MethodName: "GetApp", + Handler: _TerminalService_GetApp_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/app_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/app_pb.ts index 5764dffcd6ab8..faf0f10553d74 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/app_pb.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/app_pb.ts @@ -123,13 +123,15 @@ export interface App { * proxy hostname] if public_addr is not present. * If the app belongs to a leaf cluster, fqdn is equal to [name].[root cluster proxy hostname]. * - * fqdn is not present for SAML applications. + * fqdn is not present for SAML applications. Available only when the app was fetched through the + * ListUnifiedResources RPC. * * @generated from protobuf field: string fqdn = 10; */ fqdn: string; /** - * aws_roles is a list of AWS IAM roles for the application representing AWS console. + * aws_roles is a list of AWS IAM roles for the application representing AWS console. Available + * only when the app wast fetched through the ListUnifiedResources RPC. * * @generated from protobuf field: repeated teleport.lib.teleterm.v1.AWSRole aws_roles = 11; */ diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/gateway_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/gateway_pb.ts index f6523f7cc2210..194cc93867671 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/gateway_pb.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/gateway_pb.ts @@ -80,14 +80,15 @@ export interface Gateway { */ localPort: string; /** - * protocol is the gateway protocol + * protocol is the protocol used by the gateway. For databases, it matches the type of the + * database that the gateway targets. For apps, it's either "HTTP" or "TCP". * * @generated from protobuf field: string protocol = 7; */ protocol: string; /** * target_subresource_name points at a subresource of the remote resource, for example a - * database name on a database server. + * database name on a database server or a target port of a multi-port TCP app. * * @generated from protobuf field: string target_subresource_name = 9; */ 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 a44f65be9bd09..a64b91fe8b1d8 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 { GetAppResponse } from "./service_pb"; +import type { GetAppRequest } from "./service_pb"; import type { AuthenticateWebDeviceResponse } from "./service_pb"; import type { AuthenticateWebDeviceRequest } from "./service_pb"; import type { UpdateUserPreferencesResponse } from "./service_pb"; @@ -394,6 +396,13 @@ export interface ITerminalServiceClient { * @generated from protobuf rpc: AuthenticateWebDevice(teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest) returns (teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse); */ authenticateWebDevice(input: AuthenticateWebDeviceRequest, options?: RpcOptions): UnaryCall; + /** + * GetApp returns details of an app resource. It does not include information about AWS roles and + * FQDN. + * + * @generated from protobuf rpc: GetApp(teleport.lib.teleterm.v1.GetAppRequest) returns (teleport.lib.teleterm.v1.GetAppResponse); + */ + getApp(input: GetAppRequest, options?: RpcOptions): UnaryCall; } /** * TerminalService is used by the Electron app to communicate with the tsh daemon. @@ -815,4 +824,14 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf const method = this.methods[39], opt = this._transport.mergeOptions(options); return stackIntercept("unary", this._transport, method, opt, input); } + /** + * GetApp returns details of an app resource. It does not include information about AWS roles and + * FQDN. + * + * @generated from protobuf rpc: GetApp(teleport.lib.teleterm.v1.GetAppRequest) returns (teleport.lib.teleterm.v1.GetAppResponse); + */ + getApp(input: GetAppRequest, options?: RpcOptions): UnaryCall { + const method = this.methods[40], 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 dbce83e2bd6b9..58ff275ad0295 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 { GetAppResponse } from "./service_pb"; +import { GetAppRequest } from "./service_pb"; import { AuthenticateWebDeviceResponse } from "./service_pb"; import { AuthenticateWebDeviceRequest } from "./service_pb"; import { UpdateUserPreferencesResponse } from "./service_pb"; @@ -387,6 +389,13 @@ export interface ITerminalService extends grpc.UntypedServiceImplementation { * @generated from protobuf rpc: AuthenticateWebDevice(teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest) returns (teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse); */ authenticateWebDevice: grpc.handleUnaryCall; + /** + * GetApp returns details of an app resource. It does not include information about AWS roles and + * FQDN. + * + * @generated from protobuf rpc: GetApp(teleport.lib.teleterm.v1.GetAppRequest) returns (teleport.lib.teleterm.v1.GetAppResponse); + */ + getApp: grpc.handleUnaryCall; } /** * @grpc/grpc-js definition for the protobuf service teleport.lib.teleterm.v1.TerminalService. @@ -799,5 +808,15 @@ export const terminalServiceDefinition: grpc.ServiceDefinition requestDeserialize: bytes => AuthenticateWebDeviceRequest.fromBinary(bytes), responseSerialize: value => Buffer.from(AuthenticateWebDeviceResponse.toBinary(value)), requestSerialize: value => Buffer.from(AuthenticateWebDeviceRequest.toBinary(value)) + }, + getApp: { + path: "/teleport.lib.teleterm.v1.TerminalService/GetApp", + originalName: "GetApp", + requestStream: false, + responseStream: false, + responseDeserialize: bytes => GetAppResponse.fromBinary(bytes), + requestDeserialize: bytes => GetAppRequest.fromBinary(bytes), + responseSerialize: value => Buffer.from(GetAppResponse.toBinary(value)), + requestSerialize: value => Buffer.from(GetAppRequest.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 a2801abdca9d1..dd9139492d9c3 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts @@ -1179,6 +1179,24 @@ export interface AuthenticateWebDeviceResponse { */ confirmationToken?: DeviceConfirmationToken; } +/** + * @generated from protobuf message teleport.lib.teleterm.v1.GetAppRequest + */ +export interface GetAppRequest { + /** + * @generated from protobuf field: string app_uri = 1; + */ + appUri: string; +} +/** + * @generated from protobuf message teleport.lib.teleterm.v1.GetAppResponse + */ +export interface GetAppResponse { + /** + * @generated from protobuf field: teleport.lib.teleterm.v1.App app = 1; + */ + app?: App; +} /** * PasswordlessPrompt describes different prompts we need from users * during the passwordless login flow. @@ -5235,6 +5253,99 @@ class AuthenticateWebDeviceResponse$Type extends MessageType { + constructor() { + super("teleport.lib.teleterm.v1.GetAppRequest", [ + { no: 1, name: "app_uri", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): GetAppRequest { + const message = globalThis.Object.create((this.messagePrototype!)); + message.appUri = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetAppRequest): GetAppRequest { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* string app_uri */ 1: + message.appUri = 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: GetAppRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* string app_uri = 1; */ + if (message.appUri !== "") + writer.tag(1, WireType.LengthDelimited).string(message.appUri); + 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.GetAppRequest + */ +export const GetAppRequest = new GetAppRequest$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GetAppResponse$Type extends MessageType { + constructor() { + super("teleport.lib.teleterm.v1.GetAppResponse", [ + { no: 1, name: "app", kind: "message", T: () => App } + ]); + } + create(value?: PartialMessage): GetAppResponse { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetAppResponse): GetAppResponse { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* teleport.lib.teleterm.v1.App app */ 1: + message.app = App.internalBinaryRead(reader, reader.uint32(), options, message.app); + 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: GetAppResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* teleport.lib.teleterm.v1.App app = 1; */ + if (message.app) + App.internalBinaryWrite(message.app, writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + 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.GetAppResponse + */ +export const GetAppResponse = new GetAppResponse$Type(); /** * @generated ServiceType for protobuf service teleport.lib.teleterm.v1.TerminalService */ @@ -5278,5 +5389,6 @@ export const TerminalService = new ServiceType("teleport.lib.teleterm.v1.Termina { name: "ListUnifiedResources", options: {}, I: ListUnifiedResourcesRequest, O: ListUnifiedResourcesResponse }, { name: "GetUserPreferences", options: {}, I: GetUserPreferencesRequest, O: GetUserPreferencesResponse }, { name: "UpdateUserPreferences", options: {}, I: UpdateUserPreferencesRequest, O: UpdateUserPreferencesResponse }, - { name: "AuthenticateWebDevice", options: {}, I: AuthenticateWebDeviceRequest, O: AuthenticateWebDeviceResponse } + { name: "AuthenticateWebDevice", options: {}, I: AuthenticateWebDeviceRequest, O: AuthenticateWebDeviceResponse }, + { name: "GetApp", options: {}, I: GetAppRequest, O: GetAppResponse } ]); diff --git a/integration/appaccess/appaccess_test.go b/integration/appaccess/appaccess_test.go index dffd5f8aa1912..8bb73e091754b 100644 --- a/integration/appaccess/appaccess_test.go +++ b/integration/appaccess/appaccess_test.go @@ -831,6 +831,7 @@ func TestTCP(t *testing.T) { conn, err := net.Dial("tcp", localProxyAddress) require.NoError(t, err) + defer conn.Close() buf := make([]byte, 1024) n, err := conn.Read(buf) diff --git a/integration/appaccess/pack.go b/integration/appaccess/pack.go index 609a60adca036..a3e19634c79e0 100644 --- a/integration/appaccess/pack.go +++ b/integration/appaccess/pack.go @@ -185,6 +185,34 @@ func (p *Pack) RootAppPublicAddr() string { return p.rootAppPublicAddr } +func (p *Pack) RootTCPAppName() string { + return p.rootTCPAppName +} + +func (p *Pack) RootTCPMessage() string { + return p.rootTCPMessage +} + +func (p *Pack) RootTCPMultiPortAppName() string { + return p.rootTCPMultiPortAppName +} + +func (p *Pack) RootTCPMultiPortAppPortAlpha() int { + return p.rootTCPMultiPortAppPortAlpha +} + +func (p *Pack) RootTCPMultiPortMessageAlpha() string { + return p.rootTCPMultiPortMessageAlpha +} + +func (p *Pack) RootTCPMultiPortAppPortBeta() int { + return p.rootTCPMultiPortAppPortBeta +} + +func (p *Pack) RootTCPMultiPortMessageBeta() string { + return p.rootTCPMultiPortMessageBeta +} + func (p *Pack) RootAuthServer() *auth.Server { return p.rootCluster.Process.GetAuthServer() } @@ -201,6 +229,34 @@ func (p *Pack) LeafAppPublicAddr() string { return p.leafAppPublicAddr } +func (p *Pack) LeafTCPAppName() string { + return p.leafTCPAppName +} + +func (p *Pack) LeafTCPMessage() string { + return p.leafTCPMessage +} + +func (p *Pack) LeafTCPMultiPortAppName() string { + return p.leafTCPMultiPortAppName +} + +func (p *Pack) LeafTCPMultiPortAppPortAlpha() int { + return p.leafTCPMultiPortAppPortAlpha +} + +func (p *Pack) LeafTCPMultiPortMessageAlpha() string { + return p.leafTCPMultiPortMessageAlpha +} + +func (p *Pack) LeafTCPMultiPortAppPortBeta() int { + return p.leafTCPMultiPortAppPortBeta +} + +func (p *Pack) LeafTCPMultiPortMessageBeta() string { + return p.leafTCPMultiPortMessageBeta +} + func (p *Pack) LeafAuthServer() *auth.Server { return p.leafCluster.Process.GetAuthServer() } diff --git a/integration/proxy/proxy_helpers.go b/integration/proxy/proxy_helpers.go index 9de514caf73e1..2c1681a7b7a8c 100644 --- a/integration/proxy/proxy_helpers.go +++ b/integration/proxy/proxy_helpers.go @@ -28,6 +28,7 @@ import ( "net/http" "net/url" "path/filepath" + "strconv" "strings" "testing" "time" @@ -684,7 +685,7 @@ func mustFindKubePod(t *testing.T, tc *client.TeleportClient) { require.Equal(t, types.KindKubePod, response.Resources[0].Kind) } -func mustConnectDatabaseGateway(t *testing.T, _ *daemon.Service, gw gateway.Gateway) { +func mustConnectDatabaseGateway(ctx context.Context, t *testing.T, _ *daemon.Service, gw gateway.Gateway) { t.Helper() dbGateway, err := gateway.AsDatabase(gw) @@ -705,15 +706,15 @@ func mustConnectDatabaseGateway(t *testing.T, _ *daemon.Service, gw gateway.Gate require.NoError(t, client.Close()) } -// mustConnectAppGateway verifies that the gateway acts as an unauthenticated proxy that forwards -// requests to the app behind it. -func mustConnectAppGateway(t *testing.T, _ *daemon.Service, gw gateway.Gateway) { +// mustConnectWebAppGateway verifies that the gateway acts as an unauthenticated proxy that forwards +// requests to the web app behind it. +func mustConnectWebAppGateway(ctx context.Context, t *testing.T, _ *daemon.Service, gw gateway.Gateway) { t.Helper() - appGw, err := gateway.AsApp(gw) - require.NoError(t, err) + gatewayAddress := net.JoinHostPort(gw.LocalAddress(), gw.LocalPort()) + gatewayURL := fmt.Sprintf("http://%s", gatewayAddress) - req, err := http.NewRequest(http.MethodGet, appGw.LocalProxyURL(), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, gatewayURL, nil) require.NoError(t, err) client := &http.Client{} @@ -724,6 +725,46 @@ func mustConnectAppGateway(t *testing.T, _ *daemon.Service, gw gateway.Gateway) require.Equal(t, http.StatusOK, resp.StatusCode) } +type testGatewayConnectionFunc func(context.Context, *testing.T, *daemon.Service, gateway.Gateway) + +func makeMustConnectMultiPortTCPAppGateway(wantMessage string, otherTargetPort int, otherWantMessage string) testGatewayConnectionFunc { + return func(ctx context.Context, t *testing.T, d *daemon.Service, gw gateway.Gateway) { + t.Helper() + + gwURI := gw.URI().String() + originalTargetPort := gw.TargetSubresourceName() + makeMustConnectTCPAppGateway(wantMessage)(ctx, t, d, gw) + + _, err := d.SetGatewayTargetSubresourceName(ctx, gwURI, strconv.Itoa(otherTargetPort)) + require.NoError(t, err) + makeMustConnectTCPAppGateway(otherWantMessage)(ctx, t, d, gw) + + // Restore the original port, so that the next time the test calls this function after certs + // expire, wantMessage is going to match the port that the gateway points to. + _, err = d.SetGatewayTargetSubresourceName(ctx, gwURI, originalTargetPort) + require.NoError(t, err) + makeMustConnectTCPAppGateway(wantMessage)(ctx, t, d, gw) + } +} + +func makeMustConnectTCPAppGateway(wantMessage string) testGatewayConnectionFunc { + return func(ctx context.Context, t *testing.T, _ *daemon.Service, gw gateway.Gateway) { + t.Helper() + + gatewayAddress := net.JoinHostPort(gw.LocalAddress(), gw.LocalPort()) + conn, err := net.Dial("tcp", gatewayAddress) + require.NoError(t, err) + defer conn.Close() + + buf := make([]byte, 1024) + n, err := conn.Read(buf) + require.NoError(t, err) + + resp := strings.TrimSpace(string(buf[:n])) + require.Equal(t, wantMessage, resp) + } +} + func kubeClientForLocalProxy(t *testing.T, kubeconfigPath, teleportCluster, kubeCluster string) *kubernetes.Clientset { t.Helper() diff --git a/integration/proxy/proxy_test.go b/integration/proxy/proxy_test.go index 0dcf986d70109..7935c5aff06cb 100644 --- a/integration/proxy/proxy_test.go +++ b/integration/proxy/proxy_test.go @@ -54,6 +54,7 @@ import ( "github.com/gravitational/teleport/lib" "github.com/gravitational/teleport/lib/auth/testauthority" libclient "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/client/mfa" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/multiplexer" @@ -1315,18 +1316,29 @@ func TestALPNSNIProxyAppAccess(t *testing.T) { }) t.Run("teleterm app gateways cert renewal", func(t *testing.T) { - user, _ := pack.CreateUser(t) - tc := pack.MakeTeleportClient(t, user.GetName()) - - // test without per session MFA. - testTeletermAppGateway(t, pack, tc) + t.Run("without per-session MFA", func(t *testing.T) { + makeTC := func(t *testing.T) (*libclient.TeleportClient, mfa.WebauthnLoginFunc) { + user, _ := pack.CreateUser(t) + tc := pack.MakeTeleportClient(t, user.GetName()) + return tc, nil + } + testTeletermAppGateway(t, pack, makeTC) + testTeletermAppGatewayTargetPortValidation(t, pack, makeTC) + }) - t.Run("per session MFA", func(t *testing.T) { - // They update user's authentication to Webauthn so they must run after tests which do not use MFA. + t.Run("per-session MFA", func(t *testing.T) { + // They update clusters authentication to Webauthn so they must run after tests which do not use MFA. requireSessionMFAAuthPref(ctx, t, pack.RootAuthServer(), "127.0.0.1") requireSessionMFAAuthPref(ctx, t, pack.LeafAuthServer(), "127.0.0.1") - tc.WebauthnLogin = setupUserMFA(ctx, t, pack.RootAuthServer(), user.GetName(), "127.0.0.1") - testTeletermAppGateway(t, pack, tc) + makeTCAndWebauthnLogin := func(t *testing.T) (*libclient.TeleportClient, mfa.WebauthnLoginFunc) { + // Create a separate user for each tests to enable parallel tests that use per-session MFA. + // See the comment for webauthnLogin in setupUserMFA for more details. + user, _ := pack.CreateUser(t) + tc := pack.MakeTeleportClient(t, user.GetName()) + webauthnLogin := setupUserMFA(ctx, t, pack.RootAuthServer(), user.GetName(), "127.0.0.1") + return tc, webauthnLogin + } + testTeletermAppGateway(t, pack, makeTCAndWebauthnLogin) }) }) } diff --git a/integration/proxy/teleterm_test.go b/integration/proxy/teleterm_test.go index 399402a91958d..915fa1208a0d9 100644 --- a/integration/proxy/teleterm_test.go +++ b/integration/proxy/teleterm_test.go @@ -19,9 +19,11 @@ package proxy import ( + "cmp" "context" "errors" "net" + "strconv" "sync" "sync/atomic" "testing" @@ -50,9 +52,9 @@ import ( "github.com/gravitational/teleport/lib/auth/mocku2f" wancli "github.com/gravitational/teleport/lib/auth/webauthncli" wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" - "github.com/gravitational/teleport/lib/client" libclient "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" + "github.com/gravitational/teleport/lib/client/mfa" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/service" "github.com/gravitational/teleport/lib/service/servicecfg" @@ -168,8 +170,8 @@ func testDBGatewayCertRenewal(ctx context.Context, t *testing.T, params dbGatewa TargetURI: params.databaseURI.String(), TargetUser: params.pack.Root.User.GetName(), }, - testGatewayConnectionFunc: mustConnectDatabaseGateway, - webauthnLogin: params.webauthnLogin, + testGatewayConnection: mustConnectDatabaseGateway, + webauthnLogin: params.webauthnLogin, generateAndSetupUserCreds: func(t *testing.T, tc *libclient.TeleportClient, ttl time.Duration) { creds, err := helpers.GenerateUserCreds(helpers.UserCredsRequest{ Process: params.pack.Root.Cluster.Process, @@ -184,17 +186,16 @@ func testDBGatewayCertRenewal(ctx context.Context, t *testing.T, params dbGatewa ) } -type testGatewayConnectionFunc func(*testing.T, *daemon.Service, gateway.Gateway) - type generateAndSetupUserCredsFunc func(t *testing.T, tc *libclient.TeleportClient, ttl time.Duration) type gatewayCertRenewalParams struct { tc *libclient.TeleportClient albAddr string createGatewayParams daemon.CreateGatewayParams - testGatewayConnectionFunc testGatewayConnectionFunc + testGatewayConnection testGatewayConnectionFunc webauthnLogin libclient.WebauthnLoginFunc generateAndSetupUserCreds generateAndSetupUserCredsFunc + wantPromptMFACallCount int customCertsExpireFunc func(gateway.Gateway) expectNoRelogin bool } @@ -202,6 +203,10 @@ type gatewayCertRenewalParams struct { func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCertRenewalParams) { t.Helper() + // The test can potentially hang forever if something is wrong with the MFA prompt, add a timeout. + ctx, cancel := context.WithTimeout(ctx, time.Minute) + t.Cleanup(cancel) + tc := params.tc // Save the profile yaml file to disk as test helpers like helpers.NewClientWithCreds don't do @@ -275,7 +280,7 @@ func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCer gateway, err := daemonService.CreateGateway(ctx, params.createGatewayParams) require.NoError(t, err, trace.DebugReport(err)) - params.testGatewayConnectionFunc(t, daemonService, gateway) + params.testGatewayConnection(ctx, t, daemonService, gateway) if params.customCertsExpireFunc != nil { params.customCertsExpireFunc(gateway) @@ -292,7 +297,7 @@ func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCer // and then it will attempt to reissue the user cert using an expired user cert. // The mocked tshdEventsClient will issue a valid user cert, save it to disk, and the middleware // will let the connection through. - params.testGatewayConnectionFunc(t, daemonService, gateway) + params.testGatewayConnection(ctx, t, daemonService, gateway) expectedReloginCalls := uint32(1) if params.expectNoRelogin { @@ -303,9 +308,10 @@ func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCer require.Equal(t, uint32(0), tshdEventsService.sendNotificationCallCount.Load(), "Unexpected number of calls to TSHDEventsClient.SendNotification") if params.webauthnLogin != nil { - // There are two calls, one to issue the certs when creating the gateway and then another to - // reissue them after relogin. - require.Equal(t, uint32(2), tshdEventsService.promptMFACallCount.Load(), + // By default, there are two calls, one to issue the certs when creating the gateway and then + // another to reissue them after relogin. + wantCallCount := cmp.Or(params.wantPromptMFACallCount, 2) + require.Equal(t, uint32(wantCallCount), tshdEventsService.promptMFACallCount.Load(), "Unexpected number of calls to TSHDEventsClient.PromptMFA") } } @@ -484,9 +490,6 @@ func TestTeletermKubeGateway(t *testing.T) { t.Run("root with per-session MFA", func(t *testing.T) { profileName := mustGetProfileName(t, suite.root.Web) kubeURI := uri.NewClusterURI(profileName).AppendKube(kubeClusterName) - // The test can potentially hang forever if something is wrong with the MFA prompt, add a timeout. - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - t.Cleanup(cancel) testKubeGatewayCertRenewal(ctx, t, kubeGatewayCertRenewalParams{ suite: suite, kubeURI: kubeURI, @@ -496,9 +499,6 @@ func TestTeletermKubeGateway(t *testing.T) { t.Run("leaf with per-session MFA", func(t *testing.T) { profileName := mustGetProfileName(t, suite.root.Web) kubeURI := uri.NewClusterURI(profileName).AppendLeafCluster(suite.leaf.Secrets.SiteName).AppendKube(kubeClusterName) - // The test can potentially hang forever if something is wrong with the MFA prompt, add a timeout. - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - t.Cleanup(cancel) testKubeGatewayCertRenewal(ctx, t, kubeGatewayCertRenewalParams{ suite: suite, kubeURI: kubeURI, @@ -571,7 +571,7 @@ func testKubeGatewayCertRenewal(ctx context.Context, t *testing.T, params kubeGa }) require.NoError(t, err) - testKubeConnection := func(t *testing.T, daemonService *daemon.Service, gw gateway.Gateway) { + testKubeConnection := func(ctx context.Context, t *testing.T, daemonService *daemon.Service, gw gateway.Gateway) { t.Helper() clientOnce.Do(func() { @@ -596,10 +596,10 @@ func testKubeGatewayCertRenewal(ctx context.Context, t *testing.T, params kubeGa createGatewayParams: daemon.CreateGatewayParams{ TargetURI: params.kubeURI.String(), }, - customCertsExpireFunc: params.customCertsExpireFunc, - testGatewayConnectionFunc: testKubeConnection, - webauthnLogin: params.webauthnLogin, - expectNoRelogin: params.expectNoRelogin, + testGatewayConnection: testKubeConnection, + webauthnLogin: params.webauthnLogin, + customCertsExpireFunc: params.customCertsExpireFunc, + expectNoRelogin: params.expectNoRelogin, generateAndSetupUserCreds: func(t *testing.T, tc *libclient.TeleportClient, ttl time.Duration) { creds, err := helpers.GenerateUserCreds(helpers.UserCredsRequest{ Process: params.suite.root.Process, @@ -664,6 +664,10 @@ func setupUserMFA(ctx context.Context, t *testing.T, authServer *auth.Server, us }) require.NoError(t, err) + // webauthnLogin is not safe for concurrent use, partly due to the implementation of device, but + // mostly because Teleport itself doesn't allow for more than one in-flight MFA challenge. This is + // an arbitrary limitation which in theory we could change. But for now, parallel tests that use + // webauthnLogin must use a separate user for each test and not trigger parallel MFA prompts. webauthnLogin := func(ctx context.Context, origin string, assertion *wantypes.CredentialAssertion, prompt wancli.LoginPrompt, opts *wancli.LoginOpts) (*proto.MFAAuthenticateResponse, string, error) { car, err := device.SignAssertion(origin, assertion) if err != nil { @@ -726,34 +730,210 @@ func requireSessionMFARole(ctx context.Context, t *testing.T, authServer *auth.S require.NoError(t, err) } -func testTeletermAppGateway(t *testing.T, pack *appaccess.Pack, tc *client.TeleportClient) { +type makeTCAndWebauthnLoginFunc func(t *testing.T) (*libclient.TeleportClient, mfa.WebauthnLoginFunc) + +func testTeletermAppGateway(t *testing.T, pack *appaccess.Pack, makeTCAndWebauthnLogin makeTCAndWebauthnLoginFunc) { ctx := context.Background() t.Run("root cluster", func(t *testing.T) { - profileName := mustGetProfileName(t, pack.RootWebAddr()) - appURI := uri.NewClusterURI(profileName).AppendApp(pack.RootAppName()) + t.Parallel() - // The test can potentially hang forever if something is wrong with the MFA prompt, add a timeout. - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - t.Cleanup(cancel) - testAppGatewayCertRenewal(ctx, t, pack, tc, appURI) + t.Run("web app", func(t *testing.T) { + t.Parallel() + + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName).AppendApp(pack.RootAppName()) + + testAppGatewayCertRenewal(ctx, t, pack, makeTCAndWebauthnLogin, appURI) + }) + + t.Run("TCP app", func(t *testing.T) { + t.Parallel() + + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName).AppendApp(pack.RootTCPAppName()) + + tc, webauthnLogin := makeTCAndWebauthnLogin(t) + + testGatewayCertRenewal( + ctx, + t, + gatewayCertRenewalParams{ + tc: tc, + createGatewayParams: daemon.CreateGatewayParams{TargetURI: appURI.String()}, + testGatewayConnection: makeMustConnectTCPAppGateway(pack.RootTCPMessage()), + generateAndSetupUserCreds: pack.GenerateAndSetupUserCreds, + webauthnLogin: webauthnLogin, + }, + ) + }) + + t.Run("multi-port TCP app", func(t *testing.T) { + t.Parallel() + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName).AppendApp(pack.RootTCPMultiPortAppName()) + + tc, webauthnLogin := makeTCAndWebauthnLogin(t) + + testGatewayCertRenewal( + ctx, + t, + gatewayCertRenewalParams{ + tc: tc, + createGatewayParams: daemon.CreateGatewayParams{ + TargetURI: appURI.String(), + TargetSubresourceName: strconv.Itoa(pack.RootTCPMultiPortAppPortAlpha()), + }, + testGatewayConnection: makeMustConnectMultiPortTCPAppGateway( + pack.RootTCPMultiPortMessageAlpha(), pack.RootTCPMultiPortAppPortBeta(), pack.RootTCPMultiPortMessageBeta(), + ), + generateAndSetupUserCreds: pack.GenerateAndSetupUserCreds, + webauthnLogin: webauthnLogin, + // First MFA prompt is made when creating the gateway. Then makeMustConnectMultiPortTCPAppGateway + // changes the target port twice, which means two more prompts. + // + // Then testGatewayCertRenewal expires the certs and calls + // makeMustConnectMultiPortTCPAppGateway. The first connection refreshes the expired cert, + // then the function changes the target port twice again, resulting in two more prompts. + wantPromptMFACallCount: 3 + 3, + }, + ) + }) }) t.Run("leaf cluster", func(t *testing.T) { - profileName := mustGetProfileName(t, pack.RootWebAddr()) - appURI := uri.NewClusterURI(profileName). - AppendLeafCluster(pack.LeafAppClusterName()). - AppendApp(pack.LeafAppName()) + t.Parallel() - // The test can potentially hang forever if something is wrong with the MFA prompt, add a timeout. - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + t.Run("web app", func(t *testing.T) { + t.Parallel() + + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName). + AppendLeafCluster(pack.LeafAppClusterName()). + AppendApp(pack.LeafAppName()) + + testAppGatewayCertRenewal(ctx, t, pack, makeTCAndWebauthnLogin, appURI) + }) + + t.Run("TCP app", func(t *testing.T) { + t.Parallel() + + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName).AppendLeafCluster(pack.LeafAppClusterName()).AppendApp(pack.LeafTCPAppName()) + + tc, webauthnLogin := makeTCAndWebauthnLogin(t) + + testGatewayCertRenewal( + ctx, + t, + gatewayCertRenewalParams{ + tc: tc, + createGatewayParams: daemon.CreateGatewayParams{TargetURI: appURI.String()}, + testGatewayConnection: makeMustConnectTCPAppGateway(pack.LeafTCPMessage()), + generateAndSetupUserCreds: pack.GenerateAndSetupUserCreds, + webauthnLogin: webauthnLogin, + }, + ) + }) + + t.Run("multi-port TCP app", func(t *testing.T) { + t.Parallel() + + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName).AppendLeafCluster(pack.LeafAppClusterName()).AppendApp(pack.LeafTCPMultiPortAppName()) + + tc, webauthnLogin := makeTCAndWebauthnLogin(t) + + testGatewayCertRenewal( + ctx, + t, + gatewayCertRenewalParams{ + tc: tc, + createGatewayParams: daemon.CreateGatewayParams{ + TargetURI: appURI.String(), + TargetSubresourceName: strconv.Itoa(pack.LeafTCPMultiPortAppPortAlpha()), + }, + testGatewayConnection: makeMustConnectMultiPortTCPAppGateway( + pack.LeafTCPMultiPortMessageAlpha(), pack.LeafTCPMultiPortAppPortBeta(), pack.LeafTCPMultiPortMessageBeta(), + ), + generateAndSetupUserCreds: pack.GenerateAndSetupUserCreds, + webauthnLogin: webauthnLogin, + // First MFA prompt is made when creating the gateway. Then makeMustConnectMultiPortTCPAppGateway + // changes the target port twice, which means two more prompts. + // + // Then testGatewayCertRenewal expires the certs and calls + // makeMustConnectMultiPortTCPAppGateway. The first connection refreshes the expired cert, + // then the function changes the target port twice again, resulting in two more prompts. + wantPromptMFACallCount: 3 + 3, + }, + ) + }) + }) +} + +func testTeletermAppGatewayTargetPortValidation(t *testing.T, pack *appaccess.Pack, makeTCAndWebauthnLogin makeTCAndWebauthnLoginFunc) { + t.Run("target port validation", func(t *testing.T) { + t.Parallel() + + tc, _ := makeTCAndWebauthnLogin(t) + err := tc.SaveProfile(false /* makeCurrent */) + require.NoError(t, err) + + storage, err := clusters.NewStorage(clusters.Config{ + Dir: tc.KeysDir, + InsecureSkipVerify: tc.InsecureSkipVerify, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, + }) + require.NoError(t, err) + daemonService, err := daemon.New(daemon.Config{ + Storage: storage, + CreateTshdEventsClientCredsFunc: func() (grpc.DialOption, error) { + return grpc.WithTransportCredentials(insecure.NewCredentials()), nil + }, + CreateClientCacheFunc: func(newClient clientcache.NewClientFunc) (daemon.ClientCache, error) { + return clientcache.NewNoCache(newClient), nil + }, + KubeconfigsDir: t.TempDir(), + AgentsDir: t.TempDir(), + }) + require.NoError(t, err) + t.Cleanup(func() { + daemonService.Stop() + }) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) t.Cleanup(cancel) - testAppGatewayCertRenewal(ctx, t, pack, tc, appURI) + + // Here the test setup ends and actual test code starts. + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName).AppendApp(pack.RootTCPMultiPortAppName()) + + _, err = daemonService.CreateGateway(ctx, daemon.CreateGatewayParams{ + TargetURI: appURI.String(), + // 42 shouldn't be handed out to a non-root user when creating a listener on port 0, so it's + // unlikely that 42 is going to end up in the app spec. + TargetSubresourceName: "42", + }) + require.True(t, trace.IsBadParameter(err), "Expected BadParameter, got %v", err) + require.ErrorContains(t, err, "not included in target ports") + + gateway, err := daemonService.CreateGateway(ctx, daemon.CreateGatewayParams{ + TargetURI: appURI.String(), + TargetSubresourceName: strconv.Itoa(pack.RootTCPMultiPortAppPortAlpha()), + }) + require.NoError(t, err) + + _, err = daemonService.SetGatewayTargetSubresourceName(ctx, gateway.URI().String(), "42") + require.True(t, trace.IsBadParameter(err), "Expected BadParameter, got %v", err) + require.ErrorContains(t, err, "not included in target ports") }) } -func testAppGatewayCertRenewal(ctx context.Context, t *testing.T, pack *appaccess.Pack, tc *libclient.TeleportClient, appURI uri.ResourceURI) { +func testAppGatewayCertRenewal(ctx context.Context, t *testing.T, pack *appaccess.Pack, makeTCAndWebauthnLogin makeTCAndWebauthnLoginFunc, appURI uri.ResourceURI) { t.Helper() + tc, webauthnLogin := makeTCAndWebauthnLogin(t) testGatewayCertRenewal( ctx, @@ -763,9 +943,9 @@ func testAppGatewayCertRenewal(ctx context.Context, t *testing.T, pack *appacces createGatewayParams: daemon.CreateGatewayParams{ TargetURI: appURI.String(), }, - testGatewayConnectionFunc: mustConnectAppGateway, + testGatewayConnection: mustConnectWebAppGateway, generateAndSetupUserCreds: pack.GenerateAndSetupUserCreds, - webauthnLogin: tc.WebauthnLogin, + webauthnLogin: webauthnLogin, }, ) } diff --git a/lib/teleterm/apiserver/handler/handler_apps.go b/lib/teleterm/apiserver/handler/handler_apps.go index 846369c8bda8b..4ab5aa85403a0 100644 --- a/lib/teleterm/apiserver/handler/handler_apps.go +++ b/lib/teleterm/apiserver/handler/handler_apps.go @@ -17,12 +17,47 @@ package handler import ( + "context" + + "github.com/gravitational/trace" + "github.com/gravitational/teleport/api/types" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" + "github.com/gravitational/teleport/lib/teleterm/api/uri" "github.com/gravitational/teleport/lib/teleterm/clusters" "github.com/gravitational/teleport/lib/ui" ) +func (h *Handler) GetApp(ctx context.Context, req *api.GetAppRequest) (*api.GetAppResponse, error) { + appURI, err := uri.Parse(req.AppUri) + if err != nil { + return nil, trace.Wrap(err) + } + + proxyClient, err := h.DaemonService.GetCachedClient(ctx, appURI) + if err != nil { + return nil, trace.Wrap(err) + } + + var app types.Application + if err := clusters.AddMetadataToRetryableError(ctx, func() error { + var err error + app, err = clusters.GetApp(ctx, proxyClient.CurrentCluster(), appURI.GetAppName()) + return trace.Wrap(err) + }); err != nil { + return nil, trace.Wrap(err) + } + + clustersApp := clusters.App{ + URI: appURI, + App: app, + } + + return &api.GetAppResponse{ + App: newAPIApp(clustersApp), + }, nil +} + func newAPIApp(clusterApp clusters.App) *api.App { app := clusterApp.App diff --git a/lib/teleterm/apiserver/handler/handler_gateways.go b/lib/teleterm/apiserver/handler/handler_gateways.go index 5a303e8e45c78..dbb0de52c9363 100644 --- a/lib/teleterm/apiserver/handler/handler_gateways.go +++ b/lib/teleterm/apiserver/handler/handler_gateways.go @@ -119,7 +119,7 @@ func makeGatewayCLICommand(cmds cmd.Cmds) *api.GatewayCLICommand { // // In Connect this is used to update the db name of a db connection along with the CLI command. func (s *Handler) SetGatewayTargetSubresourceName(ctx context.Context, req *api.SetGatewayTargetSubresourceNameRequest) (*api.Gateway, error) { - gateway, err := s.DaemonService.SetGatewayTargetSubresourceName(req.GatewayUri, req.TargetSubresourceName) + gateway, err := s.DaemonService.SetGatewayTargetSubresourceName(ctx, req.GatewayUri, req.TargetSubresourceName) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/teleterm/clusters/cluster_apps.go b/lib/teleterm/clusters/cluster_apps.go index 5b92788cb15b4..cfdecb8a62f66 100644 --- a/lib/teleterm/clusters/cluster_apps.go +++ b/lib/teleterm/clusters/cluster_apps.go @@ -25,6 +25,7 @@ import ( apiclient "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" + apidefaults "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/client" @@ -55,11 +56,11 @@ type SAMLIdPServiceProvider struct { Provider types.SAMLIdPServiceProvider } -func (c *Cluster) getApp(ctx context.Context, authClient authclient.ClientI, appName string) (types.Application, error) { +func GetApp(ctx context.Context, authClient authclient.ClientI, appName string) (types.Application, error) { var app types.Application err := AddMetadataToRetryableError(ctx, func() error { apps, err := apiclient.GetAllResources[types.AppServer](ctx, authClient, &proto.ListResourcesRequest{ - Namespace: c.clusterClient.Namespace, + Namespace: apidefaults.Namespace, ResourceType: types.KindAppServer, PredicateExpression: fmt.Sprintf(`name == "%s"`, appName), }) @@ -143,3 +144,29 @@ func (c *Cluster) GetAWSRoles(app types.Application) aws.Roles { } return aws.Roles{} } + +// ValidateTargetPort parses rawTargetPort to uint32 and checks if it's included in TCP ports of app. +// It also returns an error if app doesn't have any TCP ports defined. +func ValidateTargetPort(app types.Application, rawTargetPort string) (uint32, error) { + if rawTargetPort == "" { + return 0, nil + } + + targetPort, err := parseTargetPort(rawTargetPort) + if err != nil { + return 0, trace.Wrap(err) + } + + tcpPorts := app.GetTCPPorts() + if len(tcpPorts) == 0 { + return 0, trace.BadParameter("cannot specify target port %d because app %s does not provide access to multiple ports", + targetPort, app.GetName()) + } + + if !tcpPorts.Contains(int(targetPort)) { + return 0, trace.BadParameter("port %d is not included in target ports of app %s", + targetPort, app.GetName()) + } + + return targetPort, nil +} diff --git a/lib/teleterm/clusters/cluster_gateways.go b/lib/teleterm/clusters/cluster_gateways.go index 590fa27611f21..5a08464919e11 100644 --- a/lib/teleterm/clusters/cluster_gateways.go +++ b/lib/teleterm/clusters/cluster_gateways.go @@ -21,6 +21,7 @@ package clusters import ( "context" "crypto/tls" + "strconv" "github.com/gravitational/trace" @@ -160,7 +161,7 @@ func (c *Cluster) createKubeGateway(ctx context.Context, params CreateGatewayPar func (c *Cluster) createAppGateway(ctx context.Context, params CreateGatewayParams) (gateway.Gateway, error) { appName := params.TargetURI.GetAppName() - app, err := c.getApp(ctx, params.ClusterClient.AuthClient, appName) + app, err := GetApp(ctx, params.ClusterClient.AuthClient, appName) if err != nil { return nil, trace.Wrap(err) } @@ -170,6 +171,13 @@ func (c *Cluster) createAppGateway(ctx context.Context, params CreateGatewayPara ClusterName: c.clusterClient.SiteName, URI: app.GetURI(), } + if params.TargetSubresourceName != "" { + targetPort, err := ValidateTargetPort(app, params.TargetSubresourceName) + if err != nil { + return nil, trace.Wrap(err) + } + routeToApp.TargetPort = targetPort + } var cert tls.Certificate if err := AddMetadataToRetryableError(ctx, func() error { @@ -182,6 +190,7 @@ func (c *Cluster) createAppGateway(ctx context.Context, params CreateGatewayPara gw, err := gateway.New(gateway.Config{ LocalPort: params.LocalPort, TargetURI: params.TargetURI, + TargetSubresourceName: params.TargetSubresourceName, TargetName: appName, Cert: cert, Protocol: app.GetProtocol(), @@ -195,6 +204,9 @@ func (c *Cluster) createAppGateway(ctx context.Context, params CreateGatewayPara RootClusterCACertPoolFunc: c.clusterClient.RootClusterCACertPool, ClusterName: c.Name, Username: c.status.Username, + // For multi-port TCP apps, the target port is stored in the target subresource name. Whenever + // that field is updated, the local proxy needs to generate a new cert which includes that port. + ClearCertsOnTargetSubresourceNameChange: true, }) return gw, trace.Wrap(err) } @@ -214,7 +226,7 @@ func (c *Cluster) ReissueGatewayCerts(ctx context.Context, clusterClient *client return cert, trace.Wrap(err) case g.TargetURI().IsApp(): appName := g.TargetURI().GetAppName() - app, err := c.getApp(ctx, clusterClient.AuthClient, appName) + app, err := GetApp(ctx, clusterClient.AuthClient, appName) if err != nil { return tls.Certificate{}, trace.Wrap(err) } @@ -224,6 +236,13 @@ func (c *Cluster) ReissueGatewayCerts(ctx context.Context, clusterClient *client ClusterName: c.clusterClient.SiteName, URI: app.GetURI(), } + if g.TargetSubresourceName() != "" { + targetPort, err := parseTargetPort(g.TargetSubresourceName()) + if err != nil { + return tls.Certificate{}, trace.BadParameter(err.Error()) + } + routeToApp.TargetPort = targetPort + } // The cert is returned from this function and finally set on LocalProxy by the middleware. cert, err := c.ReissueAppCert(ctx, clusterClient, routeToApp) @@ -232,3 +251,11 @@ func (c *Cluster) ReissueGatewayCerts(ctx context.Context, clusterClient *client return tls.Certificate{}, trace.NotImplemented("ReissueGatewayCerts does not support this gateway kind %v", g.TargetURI().String()) } } + +func parseTargetPort(rawTargetPort string) (uint32, error) { + targetPort, err := strconv.ParseUint(rawTargetPort, 10, 32) + if err != nil { + return 0, trace.BadParameter(err.Error()) + } + return uint32(targetPort), nil +} diff --git a/lib/teleterm/daemon/daemon.go b/lib/teleterm/daemon/daemon.go index 9b344a35104f0..67e94942c5e8e 100644 --- a/lib/teleterm/daemon/daemon.go +++ b/lib/teleterm/daemon/daemon.go @@ -334,6 +334,10 @@ func (s *Service) createGateway(ctx context.Context, params CreateGatewayParams) return gateway, nil } + if err := s.checkIfGatewayAlreadyExists(targetURI, params); err != nil { + return nil, trace.Wrap(err) + } + clusterClient, err := s.GetCachedClient(ctx, targetURI.GetClusterURI()) if err != nil { return nil, trace.Wrap(err) @@ -511,7 +515,7 @@ func (s *Service) GetGatewayCLICommand(ctx context.Context, gateway gateway.Gate // SetGatewayTargetSubresourceName updates the TargetSubresourceName field of a gateway stored in // s.gateways. -func (s *Service) SetGatewayTargetSubresourceName(gatewayURI, targetSubresourceName string) (gateway.Gateway, error) { +func (s *Service) SetGatewayTargetSubresourceName(ctx context.Context, gatewayURI, targetSubresourceName string) (gateway.Gateway, error) { s.mu.Lock() defer s.mu.Unlock() @@ -520,6 +524,35 @@ func (s *Service) SetGatewayTargetSubresourceName(gatewayURI, targetSubresourceN return nil, trace.Wrap(err) } + if err := s.checkIfGatewayAlreadyExists(gateway.TargetURI(), CreateGatewayParams{ + TargetURI: gateway.TargetURI().String(), + TargetSubresourceName: targetSubresourceName, + }); err != nil { + return nil, trace.Wrap(err) + } + + targetURI := gateway.TargetURI() + switch { + case targetURI.IsApp(): + clusterClient, err := s.GetCachedClient(ctx, targetURI) + if err != nil { + return nil, trace.Wrap(err) + } + + var app types.Application + if err := clusters.AddMetadataToRetryableError(ctx, func() error { + var err error + app, err = clusters.GetApp(ctx, clusterClient.CurrentCluster(), targetURI.GetAppName()) + return trace.Wrap(err) + }); err != nil { + return nil, trace.Wrap(err) + } + + if _, err := clusters.ValidateTargetPort(app, targetSubresourceName); err != nil { + return nil, trace.Wrap(err) + } + } + gateway.SetTargetSubresourceName(targetSubresourceName) return gateway, nil diff --git a/lib/teleterm/daemon/daemon_test.go b/lib/teleterm/daemon/daemon_test.go index fc60afe15d209..36a6a200de878 100644 --- a/lib/teleterm/daemon/daemon_test.go +++ b/lib/teleterm/daemon/daemon_test.go @@ -19,6 +19,7 @@ package daemon import ( + "cmp" "context" "errors" "net" @@ -78,11 +79,13 @@ func (m *mockGatewayCreator) CreateGateway(ctx context.Context, params clusters. KubernetesCluster: params.TargetURI.GetKubeName(), } + targetURI := params.TargetURI + config := gateway.Config{ LocalPort: params.LocalPort, TargetURI: params.TargetURI, TargetUser: params.TargetUser, - TargetName: params.TargetURI.GetDbName() + params.TargetURI.GetKubeName(), + TargetName: cmp.Or(targetURI.GetDbName(), targetURI.GetKubeName(), targetURI.GetAppName()), TargetSubresourceName: params.TargetSubresourceName, Protocol: defaults.ProtocolPostgres, Insecure: true, @@ -242,6 +245,52 @@ func TestGatewayCRUD(t *testing.T) { require.Equal(t, wantGateway, actualGateway) }, }, + { + name: "CreateGateway returns error if db gateway already exists", + gatewayNamesToCreate: []string{"gateway"}, + appendGatewayTargetURI: uri.NewClusterURI("foo").AppendDB, + testFunc: func(t *testing.T, c *gatewayCRUDTestContext, daemon *Service) { + createdGateway := c.nameToGateway["gateway"] + _, err := daemon.CreateGateway(context.Background(), CreateGatewayParams{ + TargetURI: createdGateway.TargetURI().String(), + TargetUser: createdGateway.TargetUser(), + }) + require.Error(t, err) + require.True(t, trace.IsAlreadyExists(err)) + }, + }, + { + name: "CreateGateway returns error if app gateway already exists", + gatewayNamesToCreate: []string{"gateway"}, + appendGatewayTargetURI: uri.NewClusterURI("foo").AppendApp, + testFunc: func(t *testing.T, c *gatewayCRUDTestContext, daemon *Service) { + createdGateway := c.nameToGateway["gateway"] + _, err := daemon.CreateGateway(context.Background(), CreateGatewayParams{ + TargetURI: createdGateway.TargetURI().String(), + TargetSubresourceName: createdGateway.TargetSubresourceName(), + }) + require.Error(t, err) + require.True(t, trace.IsAlreadyExists(err)) + }, + }, + { + name: "SetTargetSubresourceName returns error if db gateway already exists", + gatewayNamesToCreate: []string{"gateway"}, + appendGatewayTargetURI: uri.NewClusterURI("foo").AppendDB, + testFunc: func(t *testing.T, c *gatewayCRUDTestContext, daemon *Service) { + createdGateway := c.nameToGateway["gateway"] + _, err := daemon.CreateGateway(context.Background(), CreateGatewayParams{ + TargetURI: createdGateway.TargetURI().String(), + TargetSubresourceName: "4242", + }) + require.NoError(t, err) + + _, err = daemon.SetGatewayTargetSubresourceName(context.Background(), + createdGateway.URI().String(), "4242") + require.Error(t, err) + require.True(t, trace.IsAlreadyExists(err)) + }, + }, } for _, tt := range tests { diff --git a/lib/teleterm/daemon/gateway.go b/lib/teleterm/daemon/gateway.go new file mode 100644 index 0000000000000..d23c3279abb90 --- /dev/null +++ b/lib/teleterm/daemon/gateway.go @@ -0,0 +1,66 @@ +// 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 daemon + +import ( + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/teleterm/api/uri" + "github.com/gravitational/teleport/lib/teleterm/gateway" +) + +func (s *Service) checkIfGatewayAlreadyExists(targetURI uri.ResourceURI, params CreateGatewayParams) error { + var resourceParamsCheckFunc func(g gateway.Gateway) error + + switch { + case targetURI.IsDB(): + resourceParamsCheckFunc = func(g gateway.Gateway) error { + if g.TargetUser() == params.TargetUser { + return trace.AlreadyExists("gateway for database %s and user %s already exists", targetURI.GetDbName(), params.TargetUser) + } + return nil + } + case targetURI.IsKube(): + // Return early for kubes as kube gateways depend on s.shouldReuseGateway. + return nil + case targetURI.IsApp(): + resourceParamsCheckFunc = func(g gateway.Gateway) error { + if g.TargetSubresourceName() == params.TargetSubresourceName { + if params.TargetSubresourceName != "" { + return trace.AlreadyExists("gateway for app %s and target port %s already exists", targetURI.GetAppName(), params.TargetSubresourceName) + } else { + return trace.AlreadyExists("gateway for app %s already exists", targetURI.GetAppName()) + } + } + return nil + } + default: + return trace.NotImplemented("gateway not supported for %s", targetURI.String()) + } + + for _, g := range s.gateways { + if g.TargetURI() != targetURI { + continue + } + + if err := resourceParamsCheckFunc(g); err != nil { + return trace.Wrap(err) + } + } + + return nil +} diff --git a/lib/teleterm/gateway/app.go b/lib/teleterm/gateway/app.go index 603d640a05a9c..248f48fe873a1 100644 --- a/lib/teleterm/gateway/app.go +++ b/lib/teleterm/gateway/app.go @@ -19,8 +19,6 @@ package gateway import ( "context" "crypto/tls" - "net/url" - "strings" "github.com/gravitational/trace" @@ -33,15 +31,6 @@ type app struct { *base } -// LocalProxyURL returns the URL of the local proxy. -func (a *app) LocalProxyURL() string { - proxyURL := url.URL{ - Scheme: strings.ToLower(a.Protocol()), - Host: a.LocalAddress() + ":" + a.LocalPort(), - } - return proxyURL.String() -} - func makeAppGateway(cfg Config) (Gateway, error) { base, err := newBase(cfg) if err != nil { diff --git a/lib/teleterm/gateway/app_middleware.go b/lib/teleterm/gateway/app_middleware.go index 2da5946018147..89a695640f446 100644 --- a/lib/teleterm/gateway/app_middleware.go +++ b/lib/teleterm/gateway/app_middleware.go @@ -43,12 +43,12 @@ func (m *appMiddleware) OnNewConnection(ctx context.Context, lp *alpn.LocalProxy return nil } - // Return early and don't fire onExpiredCert if certs are invalid but not due to expiry. - if !errors.As(err, &x509.CertificateInvalidError{}) { + // Return early and don't fire onExpiredCert if certs are invalid but not due to expiry or removal. + if !errors.As(err, &x509.CertificateInvalidError{}) && !trace.IsNotFound(err) { return trace.Wrap(err) } - m.log.WithError(err).Debug("Gateway certificates have expired") + m.log.WithError(err).Debug("Gateway certificates have expired or been removed") cert, err := m.onExpiredCert(ctx) if err != nil { diff --git a/lib/teleterm/gateway/base.go b/lib/teleterm/gateway/base.go index e0a9a33cc4d86..57657aec42424 100644 --- a/lib/teleterm/gateway/base.go +++ b/lib/teleterm/gateway/base.go @@ -20,9 +20,11 @@ package gateway import ( "context" + "crypto/tls" "fmt" "net" "strconv" + "sync" "github.com/gravitational/trace" "github.com/sirupsen/logrus" @@ -89,6 +91,9 @@ func newBase(cfg Config) (*base, error) { // Close terminates gateway connection. Fails if called on an already closed gateway. func (b *base) Close() error { + b.mu.Lock() + defer b.mu.Unlock() + b.closeCancel() var errs []error @@ -158,17 +163,29 @@ func (b *base) TargetUser() string { } func (b *base) TargetSubresourceName() string { + b.mu.RLock() + defer b.mu.RUnlock() + return b.cfg.TargetSubresourceName } func (b *base) SetTargetSubresourceName(value string) { + b.mu.Lock() + defer b.mu.Unlock() b.cfg.TargetSubresourceName = value + + if b.cfg.ClearCertsOnTargetSubresourceNameChange { + b.Log().Info("Clearing cert") + b.localProxy.SetCert(tls.Certificate{}) + } } func (b *base) Log() *logrus.Entry { return b.cfg.Log } +// LocalAddress returns the local host in the net package terms (localhost or 127.0.0.1, depending +// on the platform). func (b *base) LocalAddress() string { return b.cfg.LocalAddress } @@ -187,15 +204,13 @@ func (b *base) LocalPortInt() int { } func (b *base) cloneConfig() Config { + b.mu.RLock() + defer b.mu.RUnlock() + return *b.cfg } -// Gateway describes local proxy that creates a gateway to the remote Teleport resource. -// -// Gateway is not safe for concurrent use in itself. However, all access to gateways is gated by -// daemon.Service which obtains a lock for any operation pertaining to gateways. -// -// In the future if Gateway becomes more complex it might be worthwhile to add an RWMutex to it. +// Gateway is a local proxy to a remote Teleport resource. type base struct { cfg *Config localProxy *alpn.LocalProxy @@ -206,6 +221,7 @@ type base struct { // that the local proxy is now closed and to release any resources. closeContext context.Context closeCancel context.CancelFunc + mu sync.RWMutex } type TCPPortAllocator interface { diff --git a/lib/teleterm/gateway/config.go b/lib/teleterm/gateway/config.go index c870df9075728..978cbf58d5ae3 100644 --- a/lib/teleterm/gateway/config.go +++ b/lib/teleterm/gateway/config.go @@ -91,6 +91,11 @@ type Config struct { RootClusterCACertPoolFunc alpnproxy.GetClusterCACertPoolFunc // KubeconfigsDir is the directory containing kubeconfigs for kube gateways. KubeconfigsDir string + // ClearCertsOnTargetSubresourceNameChange is useful in situations where TargetSubresourceName is + // used to generate a cert. In that case, after TargetSubresourceName is changed, the gateway will + // clear the cert from the local proxy and the middleware is going to request a new cert on the + // next connection. + ClearCertsOnTargetSubresourceNameChange bool } // OnExpiredCertFunc is the type of a function that is called when a new downstream connection is diff --git a/lib/teleterm/gateway/interfaces.go b/lib/teleterm/gateway/interfaces.go index f93528f6c5280..bc56e74b04d15 100644 --- a/lib/teleterm/gateway/interfaces.go +++ b/lib/teleterm/gateway/interfaces.go @@ -42,6 +42,8 @@ type Gateway interface { TargetSubresourceName() string SetTargetSubresourceName(value string) Log() *logrus.Entry + // LocalAddress returns the local host in the net package terms (localhost or 127.0.0.1, depending + // on the platform). LocalAddress() string LocalPort() string LocalPortInt() int @@ -97,7 +99,4 @@ type Kube interface { // App defines an app gateway. type App interface { Gateway - - // LocalProxyURL returns the URL of the local proxy. - LocalProxyURL() string } diff --git a/lib/teleterm/gateway/kube.go b/lib/teleterm/gateway/kube.go index b3e096cc31917..77e64402d3d31 100644 --- a/lib/teleterm/gateway/kube.go +++ b/lib/teleterm/gateway/kube.go @@ -201,6 +201,8 @@ func (k *kube) makeForwardProxyForKube() error { } func (k *kube) writeKubeconfig(key *keys.PrivateKey, cas map[string]tls.Certificate) error { + k.base.mu.RLock() + defer k.base.mu.RUnlock() ca, ok := cas[k.cfg.ClusterName] if !ok { return trace.BadParameter("CA for teleport cluster %q is missing", k.cfg.ClusterName) diff --git a/proto/teleport/lib/teleterm/v1/app.proto b/proto/teleport/lib/teleterm/v1/app.proto index ebd361306752a..1b5184be80bf2 100644 --- a/proto/teleport/lib/teleterm/v1/app.proto +++ b/proto/teleport/lib/teleterm/v1/app.proto @@ -75,9 +75,11 @@ message App { // proxy hostname] if public_addr is not present. // If the app belongs to a leaf cluster, fqdn is equal to [name].[root cluster proxy hostname]. // - // fqdn is not present for SAML applications. + // fqdn is not present for SAML applications. Available only when the app was fetched through the + // ListUnifiedResources RPC. string fqdn = 10; - // aws_roles is a list of AWS IAM roles for the application representing AWS console. + // aws_roles is a list of AWS IAM roles for the application representing AWS console. Available + // only when the app wast fetched through the ListUnifiedResources RPC. repeated AWSRole aws_roles = 11; // TCPPorts is a list of ports and port ranges that an app agent can forward connections to. // Only applicable to TCP App Access. diff --git a/proto/teleport/lib/teleterm/v1/gateway.proto b/proto/teleport/lib/teleterm/v1/gateway.proto index 7661a6bf31f4a..4399fcc307e26 100644 --- a/proto/teleport/lib/teleterm/v1/gateway.proto +++ b/proto/teleport/lib/teleterm/v1/gateway.proto @@ -43,12 +43,13 @@ message Gateway { string local_address = 5; // local_port is the gateway address on localhost string local_port = 6; - // protocol is the gateway protocol + // protocol is the protocol used by the gateway. For databases, it matches the type of the + // database that the gateway targets. For apps, it's either "HTTP" or "TCP". string protocol = 7; reserved 8; reserved "cli_command"; // target_subresource_name points at a subresource of the remote resource, for example a - // database name on a database server. + // database name on a database server or a target port of a multi-port TCP app. string target_subresource_name = 9; // gateway_cli_client represents a command that the user can execute to connect to the resource // through the gateway. diff --git a/proto/teleport/lib/teleterm/v1/service.proto b/proto/teleport/lib/teleterm/v1/service.proto index 20f5221d7a6a5..d816f75bd9a6f 100644 --- a/proto/teleport/lib/teleterm/v1/service.proto +++ b/proto/teleport/lib/teleterm/v1/service.proto @@ -177,6 +177,10 @@ service TerminalService { // See // https://github.com/gravitational/teleport.e/blob/master/rfd/0009e-device-trust-web-support.md#device-web-authentication. rpc AuthenticateWebDevice(AuthenticateWebDeviceRequest) returns (AuthenticateWebDeviceResponse); + + // GetApp returns details of an app resource. It does not include information about AWS roles and + // FQDN. + rpc GetApp(GetAppRequest) returns (GetAppResponse); } message EmptyResponse {} @@ -660,3 +664,11 @@ message AuthenticateWebDeviceResponse { // authentication attempt. teleport.devicetrust.v1.DeviceConfirmationToken confirmation_token = 1; } + +message GetAppRequest { + string app_uri = 1; +} + +message GetAppResponse { + App app = 1; +} diff --git a/web/.storybook/preview.tsx b/web/.storybook/preview.tsx index 405295e381006..da72200610f6d 100644 --- a/web/.storybook/preview.tsx +++ b/web/.storybook/preview.tsx @@ -26,6 +26,7 @@ import { Theme } from '../packages/design/src/theme/themes/types'; import { ConfiguredThemeProvider } from '../packages/design/src/ThemeProvider'; import history from '../packages/teleport/src/services/history/history'; import { UserContextProvider } from '../packages/teleport/src/User'; +import Logger, { ConsoleService } from '../packages/teleterm/src/logger'; import { StaticThemeProvider as TeletermThemeProvider } from '../packages/teleterm/src/ui/ThemeProvider'; import { darkTheme as teletermDarkTheme, @@ -36,6 +37,8 @@ initialize(); history.init(); +Logger.init(new ConsoleService()); + interface ThemeDecoratorProps { theme: string; title: string; diff --git a/web/packages/design/src/Input/Input.tsx b/web/packages/design/src/Input/Input.tsx index 3cf50e9d1009b..fe3b7feca968c 100644 --- a/web/packages/design/src/Input/Input.tsx +++ b/web/packages/design/src/Input/Input.tsx @@ -70,6 +70,7 @@ interface InputProps extends ColorProps, SpaceProps, WidthProps, HeightProps { inputMode?: InputMode; spellCheck?: boolean; style?: React.CSSProperties; + required?: boolean; 'aria-invalid'?: HTMLAttributes<'input'>['aria-invalid']; 'aria-describedby'?: HTMLAttributes<'input'>['aria-describedby']; @@ -170,6 +171,7 @@ const Input = forwardRef((props, ref) => { inputMode, spellCheck, style, + required, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, @@ -222,6 +224,7 @@ const Input = forwardRef((props, ref) => { inputMode, spellCheck, style, + required, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, diff --git a/web/packages/design/src/Menu/Menu.story.tsx b/web/packages/design/src/Menu/Menu.story.tsx index c7b0726ea414b..c3ba4ae802762 100644 --- a/web/packages/design/src/Menu/Menu.story.tsx +++ b/web/packages/design/src/Menu/Menu.story.tsx @@ -107,6 +107,18 @@ export const MenuItems = () => ( Amet nisi tempor + +

Label as first child

+ + Tempus ut libero + Lorem ipsum + Dolor sit amet + + Leo vitae arcu + Donec volutpat + Mauris sit + +
); diff --git a/web/packages/design/src/Menu/MenuItem.tsx b/web/packages/design/src/Menu/MenuItem.tsx index 5ccbae227c835..a9b06373bd787 100644 --- a/web/packages/design/src/Menu/MenuItem.tsx +++ b/web/packages/design/src/Menu/MenuItem.tsx @@ -71,37 +71,39 @@ const MenuItemBase = styled(Flex)` ${fromThemeBase} `; -export const MenuItemSectionLabel = styled(MenuItemBase).attrs({ - px: 2, +export const MenuItemSectionSeparator = styled.hr.attrs({ onClick: event => { // Make sure that clicks on this element don't trigger onClick set on MenuList. event.stopPropagation(); }, })` - font-weight: bold; - min-height: 16px; + background: ${props => props.theme.colors.interactive.tonal.neutral[1]}; + height: 1px; + border: 0; + font-size: 0; `; -export const MenuItemSectionSeparator = styled.hr.attrs({ +export const MenuItemSectionLabel = styled(MenuItemBase).attrs({ + px: 2, onClick: event => { // Make sure that clicks on this element don't trigger onClick set on MenuList. event.stopPropagation(); }, })` - background: ${props => props.theme.colors.interactive.tonal.neutral[1]}; - height: 1px; - border: 0; - font-size: 0; + font-weight: bold; + min-height: 16px; - // Add padding to the label for extra visual space, but only when it follows a separator. - // If a separator follows a MenuItem, there's already enough visual space, so no extra space is - // needed. The hover state of MenuItem highlights everything right from the separator start to the - // end of MenuItem. + // Add padding to the label for extra visual space, but only when it follows a separator or is the + // first child. + // + // If a separator follows a MenuItem, there's already enough visual space between MenuItem and + // separator, so no extra space is needed. The hover state of MenuItem highlights everything right + // from the separator start to the end of MenuItem. // // Padding is used instead of margin here on purpose, so that there's no empty transparent space // between Separator and Label – otherwise clicking on that space would count as a click on // MenuList and not trigger onClick set on Separator or Label. - & + ${MenuItemSectionLabel} { + ${MenuItemSectionSeparator} + &, &:first-child { padding-top: ${props => props.theme.space[1]}px; } `; diff --git a/web/packages/design/src/keyframes.ts b/web/packages/design/src/keyframes.ts index c49799db9f67f..a3a7bf96f7245 100644 --- a/web/packages/design/src/keyframes.ts +++ b/web/packages/design/src/keyframes.ts @@ -46,3 +46,7 @@ export const blink = keyframes` opacity: 100%; } `; + +export const disappear = keyframes` +to { opacity: 0; } +`; diff --git a/web/packages/shared/components/FieldInput/FieldInput.tsx b/web/packages/shared/components/FieldInput/FieldInput.tsx index 2ac28f54e810c..2f3a3eb012550 100644 --- a/web/packages/shared/components/FieldInput/FieldInput.tsx +++ b/web/packages/shared/components/FieldInput/FieldInput.tsx @@ -59,6 +59,7 @@ const FieldInput = forwardRef( toolTipContent = null, disabled = false, markAsError = false, + required = false, ...styles }, ref @@ -94,6 +95,7 @@ const FieldInput = forwardRef( size={size} aria-invalid={hasError || markAsError} aria-describedby={helperTextId} + required={required} /> ); @@ -219,7 +221,7 @@ export type FieldInputProps = BoxProps & { id?: string; name?: string; value?: string; - label?: string; + label?: React.ReactNode; helperText?: React.ReactNode; icon?: React.ComponentType; size?: InputSize; @@ -245,4 +247,5 @@ export type FieldInputProps = BoxProps & { // input box as error color before validator // runs (which marks it as error) markAsError?: boolean; + required?: boolean; }; diff --git a/web/packages/shared/components/MenuLogin/MenuLogin.tsx b/web/packages/shared/components/MenuLogin/MenuLogin.tsx index 10038eb1e4605..be99278e42f1e 100644 --- a/web/packages/shared/components/MenuLogin/MenuLogin.tsx +++ b/web/packages/shared/components/MenuLogin/MenuLogin.tsx @@ -121,18 +121,20 @@ export const MenuLogin = React.forwardRef( }, })); + const ButtonComponent = props.ButtonComponent || ButtonBorder; + return ( - - Connect + {props.buttonText || 'Connect'} - + . */ +import { ComponentPropsWithRef, ComponentType } from 'react'; + +import { ButtonBorder } from 'design/Button'; + export type LoginItem = { url: string; login: string; @@ -42,6 +46,8 @@ export type MenuLoginProps = { placeholder?: string; required?: boolean; width?: string; + ButtonComponent?: ComponentType>; + buttonText?: string; }; export type MenuLoginHandle = { diff --git a/web/packages/teleterm/src/logger.ts b/web/packages/teleterm/src/logger.ts index e215653fa1304..c4eae14b6d4d0 100644 --- a/web/packages/teleterm/src/logger.ts +++ b/web/packages/teleterm/src/logger.ts @@ -69,3 +69,19 @@ export class NullService implements LoggerService { } /* eslint-enable @typescript-eslint/no-unused-vars */ } + +export class ConsoleService implements LoggerService { + createLogger(loggerName: string): types.Logger { + return { + warn(...args: any[]) { + console.warn(loggerName, ...args); + }, + info(...args: any[]) { + console.info(loggerName, ...args); + }, + error(...args: any[]) { + console.error(loggerName, ...args); + }, + }; + } +} diff --git a/web/packages/teleterm/src/services/tshd/app.ts b/web/packages/teleterm/src/services/tshd/app.ts index 1d7c4a5c90c7e..18b7690a6acc5 100644 --- a/web/packages/teleterm/src/services/tshd/app.ts +++ b/web/packages/teleterm/src/services/tshd/app.ts @@ -114,10 +114,12 @@ export function getAppAddrWithProtocol(source: App): string { return addrWithProtocol; } +export const portRangeSeparator = '-'; + export const formatPortRange = (portRange: PortRange): string => portRange.endPort === 0 ? portRange.port.toString() - : `${portRange.port}-${portRange.endPort}`; + : `${portRange.port}${portRangeSeparator}${portRange.endPort}`; export const publicAddrWithTargetPort = (routeToApp: RouteToApp): string => routeToApp.targetPort diff --git a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts index 5ab36a94f74fb..bb49c87e475e1 100644 --- a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts +++ b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts @@ -17,6 +17,7 @@ */ import { + makeApp, makeAppGateway, makeRootCluster, } from 'teleterm/services/tshd/testHelpers'; @@ -109,6 +110,7 @@ export class MockTshClient implements TshdClient { }, }); startHeadlessWatcher = () => new MockedUnaryCall({}); + getApp = () => new MockedUnaryCall({ app: makeApp() }); } export class MockVnetClient implements VnetClient { diff --git a/web/packages/teleterm/src/services/tshd/gateway.ts b/web/packages/teleterm/src/services/tshd/gateway.ts index 266732bda6474..de2b8239694c0 100644 --- a/web/packages/teleterm/src/services/tshd/gateway.ts +++ b/web/packages/teleterm/src/services/tshd/gateway.ts @@ -16,7 +16,13 @@ * along with this program. If not, see . */ -import { GatewayTargetUri, routing } from 'teleterm/ui/uri'; +import { + GatewayTargetUri, + isAppUri, + isDatabaseUri, + isKubeUri, + routing, +} from 'teleterm/ui/uri'; import { GatewayCLICommand } from './types'; @@ -70,3 +76,29 @@ export function getTargetNameFromUri(targetUri: GatewayTargetUri): string { targetUri ); } + +/** + * getGatewayTargetUriKind is used when the callsite needs to distinguish between different kinds + * of targets that gateways support when given only its target URI. + */ +export function getGatewayTargetUriKind( + targetUri: string +): 'db' | 'kube' | 'app' { + if (isDatabaseUri(targetUri)) { + return 'db'; + } + + if (isKubeUri(targetUri)) { + return 'kube'; + } + + if (isAppUri(targetUri)) { + return 'app'; + } + + // TODO(ravicious): Optimally we'd use `targetUri satisfies never` here to have a type error when + // DocumentGateway['targetUri'] is changed. + // + // However, at the moment that field is essentially of type string, so there's not much we can do + // with regards to type safety. +} diff --git a/web/packages/teleterm/src/services/tshd/testHelpers.ts b/web/packages/teleterm/src/services/tshd/testHelpers.ts index b19fc95725192..8cb15ec3e3701 100644 --- a/web/packages/teleterm/src/services/tshd/testHelpers.ts +++ b/web/packages/teleterm/src/services/tshd/testHelpers.ts @@ -290,7 +290,7 @@ export const makeAppGateway = ( targetUri: appUri, localAddress: 'localhost', localPort: '1337', - targetSubresourceName: 'bar', + targetSubresourceName: undefined, gatewayCliCommand: { path: '', preview: 'curl http://localhost:1337', diff --git a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx index c16dd1d5fa779..842d265453d2b 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx @@ -23,7 +23,7 @@ import { MenuItemSectionLabel, MenuItemSectionSeparator, } from 'design/Menu/MenuItem'; -import { App } from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb'; +import { App, PortRange } from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb'; import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb'; import { Database } from 'gen-proto-ts/teleport/lib/teleterm/v1/database_pb'; import { Kube } from 'gen-proto-ts/teleport/lib/teleterm/v1/kube_pb'; @@ -125,8 +125,15 @@ export function ConnectAppActionButton(props: { app: App }): React.JSX.Element { connectToAppWithVnet(appContext, launchVnet, props.app, targetPort); } - function setUpGateway(): void { - setUpAppGateway(appContext, props.app, { origin: 'resource_table' }); + function setUpGateway(targetPort?: number): void { + if (!targetPort && props.app.tcpPorts.length > 0) { + targetPort = props.app.tcpPorts[0].port; + } + + setUpAppGateway(appContext, props.app.uri, { + telemetry: { origin: 'resource_table' }, + targetPort, + }); } const rootCluster = appContext.clustersService.findCluster( @@ -229,7 +236,7 @@ function AppButton(props: { cluster: Cluster; rootCluster: Cluster; connectWithVnet(targetPort?: number): void; - setUpGateway(): void; + setUpGateway(targetPort?: number): void; onLaunchUrl(): void; isVnetSupported: boolean; }) { @@ -285,37 +292,15 @@ function AppButton(props: { target="_blank" title="Launch the app in the browser" > - Set up connection + props.setUpGateway()}> + Set up connection + ); } // TCP app with VNet. if (props.isVnetSupported) { - let $targetPorts: JSX.Element; - if (props.app.tcpPorts.length) { - $targetPorts = ( - <> - - Available target ports - {props.app.tcpPorts.map((portRange, index) => ( - props.connectWithVnet(portRange.port)} - > - {formatPortRange(portRange)} - - ))} - - ); - } - return ( props.connectWithVnet()} > - Connect without VNet - {$targetPorts} + props.setUpGateway()}> + Connect without VNet + + {!!props.app.tcpPorts.length && ( + <> + + props.connectWithVnet(port)} + /> + + )} ); } - // TCP app without VNet. + // Multi-port TCP app without VNet. + if (props.app.tcpPorts.length) { + return ( + props.setUpGateway()} + > + props.setUpGateway(port)} + /> + + ); + } + + // Single-port TCP app without VNet. return ( props.setUpGateway()} textTransform="none" > Connect @@ -341,6 +353,29 @@ function AppButton(props: { ); } +const AvailableTargetPorts = (props: { + tcpPorts: PortRange[]; + onItemClick: (portRangePort: number) => void; +}) => ( + <> + Available target ports + {props.tcpPorts.map((portRange, index) => ( + props.onItemClick(portRange.port)} + > + {formatPortRange(portRange)} + + ))} + +); + export function AccessRequestButton(props: { isResourceAdded: boolean; requestStarted: boolean; diff --git a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx index 239e75dc51c45..6be35eb892d13 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx @@ -28,6 +28,10 @@ import { import { makeDatabaseGateway } from 'teleterm/services/tshd/testHelpers'; import { OfflineGateway } from '../components/OfflineGateway'; +import { + formSchema, + makeRenderFormControlsFromDefaultPort, +} from './DocumentGateway'; import { OnlineDocumentGateway } from './OnlineDocumentGateway'; type StoryProps = { @@ -99,9 +103,10 @@ export function Story(props: StoryProps) { const offlineGatewayProps: ComponentProps = { connectAttempt: makeEmptyAttempt(), reconnect: () => {}, - gatewayPort: { isSupported: true, defaultPort: '1337' }, targetName: gateway.targetName, gatewayKind: 'database', + formSchema, + renderFormControls: makeRenderFormControlsFromDefaultPort('1337'), }; if (props.connectAttempt === 'error') { diff --git a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.test.tsx b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.test.tsx new file mode 100644 index 0000000000000..2ded4c2d9b620 --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.test.tsx @@ -0,0 +1,78 @@ +/** + * 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 . + */ + +import userEvent from '@testing-library/user-event'; + +import { render, screen } from 'design/utils/testing'; + +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; +import { + makeDatabaseGateway, + makeRootCluster, +} from 'teleterm/services/tshd/testHelpers'; +import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; +import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; +import type * as docs from 'teleterm/ui/services/workspacesService'; +import { DatabaseUri } from 'teleterm/ui/uri'; + +import { MockWorkspaceContextProvider } from '../fixtures/MockWorkspaceContextProvider'; +import { DocumentGateway } from './DocumentGateway'; + +test('it allows reconnecting when the gateway fails to be created', async () => { + const user = userEvent.setup(); + + const appContext = new MockAppContext(); + const cluster = makeRootCluster(); + const gateway = makeDatabaseGateway(); + const doc: docs.DocumentGateway = { + uri: '/docs/1', + kind: 'doc.gateway', + targetName: gateway.targetName, + targetUri: gateway.targetUri as DatabaseUri, + targetUser: gateway.targetUser, + targetSubresourceName: gateway.targetSubresourceName, + gatewayUri: gateway.uri, + origin: 'resource_table', + title: '', + status: '', + }; + appContext.addRootClusterWithDoc(cluster, doc); + + jest + .spyOn(appContext.tshd, 'createGateway') + .mockReturnValueOnce( + new MockedUnaryCall(undefined, new Error('Something went wrong')) + ) + .mockReturnValueOnce(new MockedUnaryCall(gateway)); + + render( + + + + + + ); + + expect( + await screen.findByText('Could not establish the connection') + ).toBeInTheDocument(); + + await user.click(screen.getByText('Reconnect')); + + expect(await screen.findByText('Close Connection')).toBeInTheDocument(); +}); diff --git a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.tsx b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.tsx index 6c0a3585bb98a..cb884bff8a924 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.tsx +++ b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.tsx @@ -16,11 +16,15 @@ * along with this program. If not, see . */ +import { useMemo } from 'react'; +import { z } from 'zod'; + import { getCliCommandArgv0 } from 'teleterm/services/tshd/gateway'; import Document from 'teleterm/ui/Document'; import * as types from 'teleterm/ui/services/workspacesService'; -import { OfflineGateway } from '../components/OfflineGateway'; +import { PortFieldInput } from '../components/FieldInputs'; +import { FormFields, OfflineGateway } from '../components/OfflineGateway'; import { useWorkspaceContext } from '../Documents'; import { OnlineDocumentGateway } from './OnlineDocumentGateway'; import { useGateway } from './useGateway'; @@ -63,15 +67,21 @@ export function DocumentGateway(props: { documentsService.setLocation(cliDoc.uri); }; + const renderFormControls = useMemo( + () => makeRenderFormControlsFromDefaultPort(defaultPort), + [defaultPort] + ); + if (!connected) { return ( ); @@ -92,3 +102,16 @@ export function DocumentGateway(props: { ); } + +export const formSchema = z.object({ [FormFields.LocalPort]: z.string() }); + +export const makeRenderFormControlsFromDefaultPort = + (defaultPort: string) => (isProcessing: boolean) => ( + + ); diff --git a/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts b/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts index 1c08bae058742..d80c8cfacaf9f 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts +++ b/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts @@ -24,12 +24,16 @@ import { useAsync } from 'shared/hooks/useAsync'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import { useWorkspaceContext } from 'teleterm/ui/Documents'; import { useStoreSelector } from 'teleterm/ui/hooks/useStoreSelector'; -import { DocumentGateway } from 'teleterm/ui/services/workspacesService'; +import { + DocumentGateway, + getDocumentGatewayTitle, +} from 'teleterm/ui/services/workspacesService'; import { isAppUri, isDatabaseUri } from 'teleterm/ui/uri'; import { retryWithRelogin } from 'teleterm/ui/utils'; export function useGateway(doc: DocumentGateway) { const ctx = useAppContext(); + const { clustersService, usageService } = ctx; const { documentsService } = useWorkspaceContext(); // The port to show as default in the input field in case creating a gateway fails. // This is typically the case if someone reopens the app and the port of the gateway is already @@ -45,81 +49,118 @@ export function useGateway(doc: DocumentGateway) { ); const connected = !!gateway; - const [connectAttempt, createGateway] = useAsync(async (port: string) => { - documentsService.update(doc.uri, { status: 'connecting' }); - let gw: Gateway; + const [connectAttempt, createGateway] = useAsync( + useCallback( + async (args: { localPort?: string; targetSubresourceName?: string }) => { + documentsService.update(doc.uri, { status: 'connecting' }); + let gw: Gateway; - try { - gw = await retryWithRelogin(ctx, doc.targetUri, () => - ctx.clustersService.createGateway({ - targetUri: doc.targetUri, - localPort: port, - targetUser: doc.targetUser, - targetSubresourceName: doc.targetSubresourceName, - }) - ); - } catch (error) { - documentsService.update(doc.uri, { status: 'error' }); - throw error; - } - documentsService.update(doc.uri, { - gatewayUri: gw.uri, - // Set the port on doc to match the one returned from the daemon. Teleterm doesn't let the - // user provide a port for the gateway, so instead we have to let the daemon use a random - // one. - // - // Setting it here makes it so that on app restart, Teleterm will restart the proxy with the - // same port number. - port: gw.localPort, - status: 'connected', - }); - if (isDatabaseUri(doc.targetUri)) { - ctx.usageService.captureProtocolUse({ - uri: doc.targetUri, - protocol: 'db', - origin: doc.origin, - accessThrough: 'local_proxy', - }); - } - if (isAppUri(doc.targetUri)) { - ctx.usageService.captureProtocolUse({ - uri: doc.targetUri, - protocol: 'app', - origin: doc.origin, - accessThrough: 'local_proxy', - }); - } - }); + try { + gw = await retryWithRelogin(ctx, doc.targetUri, () => + clustersService.createGateway({ + targetUri: doc.targetUri, + localPort: args.localPort, + targetUser: doc.targetUser, + targetSubresourceName: + args.targetSubresourceName || doc.targetSubresourceName, + }) + ); + } catch (error) { + documentsService.update(doc.uri, { status: 'error' }); + throw error; + } + documentsService.update(doc.uri, draft => { + const draftDoc = draft as DocumentGateway; + draftDoc.gatewayUri = gw.uri; + // Set the port on doc to match the one returned from the daemon. By default, + // createGateway is called with an empty localPort, so the daemon creates a listener on a + // random port. + // + // Setting it here makes it so that on app restart, Teleterm will restart the proxy with the + // same port number. + // + // Alternatively, if createGateway was called from OfflineGateway, this will persist in + // the doc the local port chosen by the user. + draftDoc.port = gw.localPort; + // targetSubresourceName needs to be updated here in case the createGateway function was + // called from OfflineGateway. + draftDoc.targetSubresourceName = gw.targetSubresourceName; + draftDoc.status = 'connected'; + // The title might need to be changed if OfflineGateway changed gateway params. + draftDoc.title = getDocumentGatewayTitle(draftDoc); + }); + if (isDatabaseUri(doc.targetUri)) { + usageService.captureProtocolUse({ + uri: doc.targetUri, + protocol: 'db', + origin: doc.origin, + accessThrough: 'local_proxy', + }); + } + if (isAppUri(doc.targetUri)) { + usageService.captureProtocolUse({ + uri: doc.targetUri, + protocol: 'app', + origin: doc.origin, + accessThrough: 'local_proxy', + }); + } + }, + [clustersService, ctx, doc, documentsService, usageService] + ) + ); const [disconnectAttempt, disconnect] = useAsync(async () => { - await ctx.clustersService.removeGateway(doc.gatewayUri); + await clustersService.removeGateway(doc.gatewayUri); documentsService.close(doc.uri); }); const [changeTargetSubresourceNameAttempt, changeTargetSubresourceName] = - useAsync(async (name: string) => { - const updatedGateway = - await ctx.clustersService.setGatewayTargetSubresourceName( - doc.gatewayUri, - name - ); + useAsync( + useCallback( + (name: string) => + retryWithRelogin(ctx, doc.targetUri, async () => { + const updatedGateway = + await clustersService.setGatewayTargetSubresourceName( + doc.gatewayUri, + name + ); - documentsService.update(doc.uri, { - targetSubresourceName: updatedGateway.targetSubresourceName, - }); - }); + documentsService.update(doc.uri, draft => { + const draftDoc = draft as DocumentGateway; - const [changePortAttempt, changePort] = useAsync(async (port: string) => { - const updatedGateway = await ctx.clustersService.setGatewayLocalPort( - doc.gatewayUri, - port + draftDoc.targetSubresourceName = + updatedGateway.targetSubresourceName; + draftDoc.title = getDocumentGatewayTitle(draftDoc); + }); + }), + [ + clustersService, + documentsService, + doc.uri, + doc.gatewayUri, + ctx, + doc.targetUri, + ] + ) ); - documentsService.update(doc.uri, { - targetSubresourceName: updatedGateway.targetSubresourceName, - port: updatedGateway.localPort, - }); - }); + const [changePortAttempt, changePort] = useAsync( + useCallback( + async (port: string) => { + const updatedGateway = await clustersService.setGatewayLocalPort( + doc.gatewayUri, + port + ); + + documentsService.update(doc.uri, { + targetSubresourceName: updatedGateway.targetSubresourceName, + port: updatedGateway.localPort, + }); + }, + [clustersService, documentsService, doc.uri, doc.gatewayUri] + ) + ); useEffect( function createGatewayOnMount() { @@ -127,7 +168,7 @@ export function useGateway(doc: DocumentGateway) { // to open DocumentGateway while the gateway is already running. In that scenario, we must // not attempt to create a gateway. if (!gateway && connectAttempt.status === '') { - createGateway(doc.port); + createGateway({ localPort: doc.port }); } }, // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx index 1c2981ce9f42b..f6b18799b65c6 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx @@ -16,105 +16,329 @@ * along with this program. If not, see . */ -import { useMemo, useRef } from 'react'; +import { + ChangeEvent, + ChangeEventHandler, + PropsWithChildren, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import styled from 'styled-components'; import { Alert, - Box, ButtonSecondary, + disappear, Flex, H1, - Indicator, Link, + rotate360, Text, } from 'design'; +import { Check, Spinner } from 'design/Icon'; import { Gateway } from 'gen-proto-ts/teleport/lib/teleterm/v1/gateway_pb'; +import { LoginItem, MenuLogin } from 'shared/components/MenuLogin'; import { TextSelectCopy } from 'shared/components/TextSelectCopy'; import Validation from 'shared/components/Validation'; -import { Attempt } from 'shared/hooks/useAsync'; +import { Attempt, useAsync } from 'shared/hooks/useAsync'; import { debounce } from 'shared/utils/highbar'; -import { PortFieldInput } from '../components/FieldInputs'; +import { + formatPortRange, + portRangeSeparator, +} from 'teleterm/services/tshd/app'; +import { useAppContext } from 'teleterm/ui/appContextProvider'; +import { PortFieldInput } from 'teleterm/ui/components/FieldInputs'; +import { useLogger } from 'teleterm/ui/hooks/useLogger'; +import { setUpAppGateway } from 'teleterm/ui/services/workspacesService'; +import { retryWithRelogin } from 'teleterm/ui/utils'; export function AppGateway(props: { gateway: Gateway; disconnectAttempt: Attempt; - changePort(port: string): void; - changePortAttempt: Attempt; + changeLocalPort(port: string): void; + changeLocalPortAttempt: Attempt; + changeTargetPort(port: string): void; + changeTargetPortAttempt: Attempt; disconnect(): void; }) { const { gateway } = props; - const formRef = useRef(); + const ctx = useAppContext(); + const { tshd } = ctx; + const { targetUri } = gateway; + const logger = useLogger('AppGateway'); - const { changePort } = props; - const handleChangePort = useMemo(() => { - return debounce((value: string) => { - if (formRef.current.reportValidity()) { - changePort(value); - } - }, 1000); - }, [changePort]); + const { + changeLocalPort, + changeLocalPortAttempt, + changeTargetPort, + changeTargetPortAttempt, + disconnectAttempt, + } = props; + // It must be possible to update local port while target port is invalid, hence why + // useDebouncedPortChangeHandler checks the validity of only one input at a time. Otherwise the UI + // would lose updates to the local port while the target port was invalid. + const handleLocalPortChange = useDebouncedPortChangeHandler(changeLocalPort); + const handleTargetPortChange = + useDebouncedPortChangeHandler(changeTargetPort); let address = `${gateway.localAddress}:${gateway.localPort}`; if (gateway.protocol === 'HTTP') { address = `http://${address}`; } + // AppGateway doesn't have access to the app resource itself, so it has to decide whether the + // app is multi-port or not in some other way. + // For multi-port apps, DocumentGateway comes with targetSubresourceName prefilled to the first + // port number found in TCP ports. Single-port apps have this field empty. + // So, if targetSubresourceName is present, then the app must be multi-port. In this case, the + // user is free to change it and can never provide an empty targetSubresourceName. + // When the app is not multi-port, targetSubresourceName is empty and the user cannot change it. + const isMultiPort = + gateway.protocol === 'TCP' && gateway.targetSubresourceName; + const targetPortRef = useRef(null); + + const [tcpPortsAttempt, getTcpPorts] = useAsync( + useCallback( + () => + retryWithRelogin(ctx, targetUri, () => + tshd + .getApp({ appUri: targetUri }) + .then(({ response }) => response.app.tcpPorts) + ), + [ctx, targetUri, tshd] + ) + ); + const currentTargetPort = parseInt(gateway.targetSubresourceName); + const getTcpPortsForMenuLogin: () => Promise = + useCallback(async () => { + const [tcpPorts, error] = await getTcpPorts(); + + if (error) { + throw error; + } + + return tcpPorts + .filter( + portRange => + // Filter out single-port port ranges that are equal to the current port. + portRange.endPort !== 0 || portRange.port != currentTargetPort + ) + .map(portRange => ({ + login: formatPortRange(portRange), + url: '', + })); + }, [getTcpPorts, currentTargetPort]); + + const onPortRangeSelect = (_, formattedPortRange: string) => { + const firstPort = formattedPortRange.split(portRangeSeparator)[0]; + const targetPort = parseInt(firstPort); + + if (Number.isNaN(targetPort)) { + logger.error('Not a number', firstPort); + return; + } + + setUpAppGateway(ctx, targetUri, { + telemetry: { origin: 'resource_table' }, + targetPort, + }); + }; + return ( - - -

App Connection

- - Close Connection - -
+ + + +

App Connection

+ + {isMultiPort && ( + + )} + + Close Connection + + +
- {props.disconnectAttempt.status === 'error' && ( - - Could not close the connection - - )} - - - - handleChangePort(e.target.value)} - mb={2} - /> - - {props.changePortAttempt.status === 'processing' && ( - + {disconnectAttempt.status === 'error' && ( + + Could not close the connection + )} + + + + + } + defaultValue={gateway.localPort} + onChange={handleLocalPortChange} + mb={0} + /> + {isMultiPort && ( + + } + required + defaultValue={gateway.targetSubresourceName} + onChange={handleTargetPortChange} + mb={0} + ref={targetPortRef} + /> + )} + + - Access the app at: - - - {props.changePortAttempt.status === 'error' && ( - - Could not change the port number - - )} - - - The connection is made through an authenticated proxy so no extra - credentials are necessary. See{' '} - - the documentation - {' '} - for more details. - -
+ +
+ Access the app at: + +
+ + {changeLocalPortAttempt.status === 'error' && ( + + Could not change the local port + + )} + + {changeTargetPortAttempt.status === 'error' && ( + + Could not change the target port + + )} + + {tcpPortsAttempt.status === 'error' && ( + + Could not fetch available target ports + + )} + + + The connection is made through an authenticated proxy so no extra + credentials are necessary. See{' '} + + the documentation + {' '} + for more details. + +
+ ); } + +const LabelWithAttemptStatus = (props: { + text: string; + attempt: Attempt; +}) => ( + + {props.text} + {props.attempt.status === 'processing' && ( + + )} + {props.attempt.status === 'success' && ( + // CSS animations are repeated whenever the parent goes from `display: none` to something + // else. As a result, we need to unmount the animated check so that the animation is not + // repeated when the user switches to this tab. + // https://www.w3.org/TR/css-animations-1/#example-4e34d7ba + + + + )} + +); + +/** + * useDebouncedPortChangeHandler returns a debounced change handler that calls the change function + * only if the input from which the event originated is valid. + */ +const useDebouncedPortChangeHandler = ( + changeFunc: (port: string) => void +): ChangeEventHandler => + useMemo( + () => + debounce((event: ChangeEvent) => { + if (event.target.reportValidity()) { + changeFunc(event.target.value); + } + }, 1000), + [changeFunc] + ); + +const AnimatedSpinner = styled(Spinner)` + animation: ${rotate360} 1.5s infinite linear; + // The spinner needs to be positioned absolutely so that the fact that it's spinning + // doesn't affect the size of the parent. + position: absolute; + right: 0; + top: 0; +`; + +const disappearanceDelayMs = 1000; +const disappearanceDurationMs = 200; + +const DisappearingCheck = styled(Check)` + opacity: 1; + animation: ${disappear}; + animation-delay: ${disappearanceDelayMs}ms; + animation-duration: ${disappearanceDurationMs}ms; + animation-fill-mode: forwards; +`; + +const UnmountAfter = ({ + timeoutMs, + children, +}: PropsWithChildren<{ timeoutMs: number }>) => { + const [isMounted, setIsMounted] = useState(true); + + useEffect(() => { + const timeout = setTimeout(() => { + setIsMounted(false); + }, timeoutMs); + + return () => { + clearTimeout(timeout); + }; + }, [timeoutMs]); + + return isMounted ? children : null; +}; diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx index c0b4ec802b28e..b140fe1aac5c8 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx @@ -19,10 +19,10 @@ import { Meta } from '@storybook/react'; import { Flex } from 'design'; -import { wait } from 'shared/utils/wait'; +import { usePromiseRejectedOnUnmount, wait } from 'shared/utils/wait'; import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; -import { makeAppGateway } from 'teleterm/services/tshd/testHelpers'; +import { makeApp, makeAppGateway } from 'teleterm/services/tshd/testHelpers'; import { DocumentGatewayApp } from 'teleterm/ui/DocumentGatewayApp/DocumentGatewayApp'; import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; @@ -30,10 +30,12 @@ import { MockWorkspaceContextProvider } from 'teleterm/ui/fixtures/MockWorkspace import * as types from 'teleterm/ui/services/workspacesService'; type StoryProps = { - appType: 'web' | 'tcp'; + appType: 'web' | 'tcp' | 'tcp-multi-port'; online: boolean; - changePort: 'succeed' | 'throw-error'; + changeLocalPort: 'succeed' | 'throw-error'; + changeTargetPort: 'succeed' | 'throw-error'; disconnect: 'succeed' | 'throw-error'; + getTcpPorts: 'succeed' | 'throw-error' | 'processing' | 'many-ports'; }; const meta: Meta = { @@ -42,9 +44,14 @@ const meta: Meta = { argTypes: { appType: { control: { type: 'radio' }, - options: ['web', 'tcp'], + options: ['web', 'tcp', 'tcp-multi-port'], }, - changePort: { + changeLocalPort: { + if: { arg: 'online' }, + control: { type: 'radio' }, + options: ['succeed', 'throw-error'], + }, + changeTargetPort: { if: { arg: 'online' }, control: { type: 'radio' }, options: ['succeed', 'throw-error'], @@ -54,12 +61,20 @@ const meta: Meta = { control: { type: 'radio' }, options: ['succeed', 'throw-error'], }, + getTcpPorts: { + if: { arg: 'online' }, + control: { type: 'radio' }, + options: ['succeed', 'throw-error', 'processing', 'many-ports'], + description: 'Used only for multi-port TCP apps.', + }, }, args: { appType: 'web', online: true, - changePort: 'succeed', + changeLocalPort: 'succeed', + changeTargetPort: 'succeed', disconnect: 'succeed', + getTcpPorts: 'succeed', }, }; export default meta; @@ -70,6 +85,10 @@ export function Story(props: StoryProps) { if (props.appType === 'tcp') { gateway.protocol = 'TCP'; } + if (props.appType === 'tcp-multi-port') { + gateway.protocol = 'TCP'; + gateway.targetSubresourceName = '4242'; + } const documentGateway: types.DocumentGateway = { kind: 'doc.gateway', targetUri: '/clusters/bar/apps/quux', @@ -80,10 +99,16 @@ export function Story(props: StoryProps) { targetUser: '', status: '', targetName: 'quux', + targetSubresourceName: undefined, }; if (!props.online) { documentGateway.gatewayUri = undefined; } + if (props.appType === 'tcp-multi-port') { + documentGateway.targetSubresourceName = '4242'; + } + + const infinitePromise = usePromiseRejectedOnUnmount(); const appContext = new MockAppContext(); appContext.workspacesService.setState(draftState => { @@ -105,8 +130,26 @@ export function Story(props: StoryProps) { wait(1000).then( () => new MockedUnaryCall( - { ...gateway, localPort }, - props.changePort === 'throw-error' + { + ...appContext.clustersService.findGateway(gateway.uri), + localPort, + }, + props.changeLocalPort === 'throw-error' + ? new Error('something went wrong') + : undefined + ) + ); + appContext.tshd.setGatewayTargetSubresourceName = ({ + targetSubresourceName, + }) => + wait(1000).then( + () => + new MockedUnaryCall( + { + ...appContext.clustersService.findGateway(gateway.uri), + targetSubresourceName, + }, + props.changeTargetPort === 'throw-error' ? new Error('something went wrong') : undefined ) @@ -121,6 +164,35 @@ export function Story(props: StoryProps) { : undefined ) ); + + if (props.getTcpPorts === 'processing') { + appContext.tshd.getApp = () => infinitePromise; + } else { + let tcpPorts = [ + { port: 1337, endPort: 4242 }, + { port: 4242, endPort: 0 }, + { port: 17231, endPort: 0 }, + { port: 27381, endPort: 28400 }, + ]; + if (props.getTcpPorts === 'many-ports') { + tcpPorts = new Array(9).fill(tcpPorts).flat(); + } + + appContext.tshd.getApp = () => + wait(500).then( + () => + new MockedUnaryCall( + { + app: makeApp({ + tcpPorts, + }), + }, + props.getTcpPorts === 'throw-error' + ? new Error('something went wrong') + : undefined + ) + ); + } } else { appContext.clustersService.createGateway = () => Promise.reject(new Error('failed to create gateway')); diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.test.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.test.tsx new file mode 100644 index 0000000000000..91e060966233b --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.test.tsx @@ -0,0 +1,170 @@ +/** + * 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 . + */ + +import userEvent from '@testing-library/user-event'; + +import { render, screen } from 'design/utils/testing'; +import { Gateway } from 'gen-proto-ts/teleport/lib/teleterm/v1/gateway_pb'; + +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; +import { + makeAppGateway, + makeRootCluster, +} from 'teleterm/services/tshd/testHelpers'; +import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; +import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; +import { MockWorkspaceContextProvider } from 'teleterm/ui/fixtures/MockWorkspaceContextProvider'; +import type * as docs from 'teleterm/ui/services/workspacesService'; +import { AppUri } from 'teleterm/ui/uri'; + +import { DocumentGatewayApp } from './DocumentGatewayApp'; + +beforeEach(() => { + jest.restoreAllMocks(); +}); + +describe('reconnecting when the gateway fails to be created', () => { + const tests: Array<{ + name: string; + gateway: Gateway; + }> = [ + { + name: 'web app', + gateway: makeAppGateway({ + protocol: 'HTTP', + targetSubresourceName: undefined, + }), + }, + { + name: 'single-port TCP app', + gateway: makeAppGateway({ + protocol: 'TCP', + targetSubresourceName: undefined, + }), + }, + { + name: 'multi-port TCP app', + gateway: makeAppGateway({ + protocol: 'TCP', + targetSubresourceName: '1337', + }), + }, + ]; + + test.each(tests)('$name', async ({ gateway }) => { + const user = userEvent.setup(); + + const appContext = new MockAppContext(); + const cluster = makeRootCluster(); + const doc: docs.DocumentGateway = { + uri: '/docs/1', + kind: 'doc.gateway', + targetName: gateway.targetName, + targetUri: gateway.targetUri as AppUri, + targetUser: gateway.targetUser, + targetSubresourceName: gateway.targetSubresourceName, + gatewayUri: gateway.uri, + origin: 'resource_table', + title: '', + status: '', + }; + appContext.addRootClusterWithDoc(cluster, doc); + + jest + .spyOn(appContext.tshd, 'createGateway') + .mockReturnValueOnce( + new MockedUnaryCall(undefined, new Error('Something went wrong')) + ) + .mockReturnValueOnce(new MockedUnaryCall(gateway)); + + render( + + + + + + ); + + expect( + await screen.findByText('Could not establish the connection') + ).toBeInTheDocument(); + + await user.click(screen.getByText('Reconnect')); + + expect(await screen.findByText('Close Connection')).toBeInTheDocument(); + }); + + it('allows changing the target port for multi-port TCP apps', async () => { + const user = userEvent.setup(); + + const appContext = new MockAppContext(); + const cluster = makeRootCluster(); + const gateway = makeAppGateway({ + protocol: 'TCP', + targetSubresourceName: '1337', + }); + const doc: docs.DocumentGateway = { + uri: '/docs/1', + kind: 'doc.gateway', + targetName: gateway.targetName, + targetUri: gateway.targetUri as AppUri, + targetUser: gateway.targetUser, + targetSubresourceName: '1337', + gatewayUri: gateway.uri, + origin: 'resource_table', + title: '', + status: '', + }; + appContext.addRootClusterWithDoc(cluster, doc); + + jest + .spyOn(appContext.tshd, 'createGateway') + .mockReturnValueOnce( + new MockedUnaryCall(undefined, new Error('Something went wrong')) + ) + .mockImplementationOnce( + async req => new MockedUnaryCall({ ...gateway, ...req }) + ); + + render( + + + + + + ); + + expect( + await screen.findByText('Could not establish the connection') + ).toBeInTheDocument(); + + const targetPortInput = screen.getByLabelText('Target Port'); + await user.clear(targetPortInput); + await user.type(targetPortInput, '4242'); + await user.click(screen.getByText('Reconnect')); + + expect(await screen.findByText('Close Connection')).toBeInTheDocument(); + expect(screen.getByLabelText('Target Port')).toHaveValue(4242); + + expect(appContext.tshd.createGateway).toHaveBeenLastCalledWith( + expect.objectContaining({ + targetSubresourceName: '4242', + }) + ); + }); +}); diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx index ba70a7dfbdbe3..a49270c1274c4 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx @@ -15,10 +15,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import { ComponentProps } from 'react'; +import { z } from 'zod'; + import Document from 'teleterm/ui/Document'; import { DocumentGateway } from 'teleterm/ui/services/workspacesService'; -import { OfflineGateway } from '../components/OfflineGateway'; +import { PortFieldInput } from '../components/FieldInputs'; +import { FormFields, OfflineGateway } from '../components/OfflineGateway'; import { useGateway } from '../DocumentGateway/useGateway'; import { AppGateway } from './AppGateway'; @@ -29,34 +33,83 @@ export function DocumentGatewayApp(props: { const { doc } = props; const { gateway, - changePort, - changePortAttempt, + defaultPort, + changePort: changeLocalPort, + changePortAttempt: changeLocalPortAttempt, connected, connectAttempt, disconnect, disconnectAttempt, reconnect, + changeTargetSubresourceName: changeTargetPort, + changeTargetSubresourceNameAttempt: changeTargetPortAttempt, } = useGateway(doc); + const isMultiPort = !!doc.targetSubresourceName; + // TypeScript doesn't seem to be able to properly infer a simpler construct such as + // + // isMultiPort ? multiPortSchema : singlePortSchema + // + // This code would always infer formSchema to be that of the simpler type (singlePortSchema), so + // any errors in multiPortSchema would not be caught. + let formSchema: ComponentProps['formSchema'] = + singlePortSchema; + if (isMultiPort) { + formSchema = multiPortSchema; + } + return ( - + ); } + +const singlePortSchema = z.object({ [FormFields.LocalPort]: z.string() }); +const multiPortSchema = singlePortSchema.extend({ + [FormFields.TargetSubresourceName]: z.string(), +}); diff --git a/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.tsx b/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.tsx index 2918f84f32819..61d018b54a67c 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.tsx @@ -50,10 +50,10 @@ export const DocumentGatewayCliClient = (props: { const { doc, visible } = props; const [hasRenderedTerminal, setHasRenderedTerminal] = useState(false); - const gateway = clustersService.findGatewayByConnectionParams( - doc.targetUri, - doc.targetUser - ); + const gateway = clustersService.findGatewayByConnectionParams({ + targetUri: doc.targetUri, + targetUser: doc.targetUser, + }); // Once we render the terminal, we want to keep it visible. Otherwise removing the gateway would // mean that this document would immediately unmount DocumentTerminal and close the PTY. diff --git a/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.test.tsx b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.test.tsx new file mode 100644 index 0000000000000..a673c316a3f00 --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.test.tsx @@ -0,0 +1,84 @@ +/** + * 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 . + */ +import 'jest-canvas-mock'; + +import userEvent from '@testing-library/user-event'; + +import { render, screen } from 'design/utils/testing'; + +import Logger, { NullService } from 'teleterm/logger'; +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; +import { + makeKubeGateway, + makeRootCluster, +} from 'teleterm/services/tshd/testHelpers'; +import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; +import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; +import type * as docs from 'teleterm/ui/services/workspacesService'; +import { KubeUri, routing } from 'teleterm/ui/uri'; + +import { MockWorkspaceContextProvider } from '../fixtures/MockWorkspaceContextProvider'; +import { DocumentGatewayKube } from './DocumentGatewayKube'; + +beforeAll(() => { + Logger.init(new NullService()); +}); + +test('it allows reconnecting when the gateway fails to be created', async () => { + const user = userEvent.setup(); + + const appContext = new MockAppContext(); + const cluster = makeRootCluster(); + const gateway = makeKubeGateway(); + const doc: docs.DocumentGatewayKube = { + uri: '/docs/1', + kind: 'doc.gateway_kube', + targetUri: gateway.targetUri as KubeUri, + rootClusterId: routing.parseClusterUri(cluster.uri).params['rootClusterId'], + leafClusterId: undefined, + origin: 'resource_table', + title: '', + status: '', + }; + appContext.addRootClusterWithDoc(cluster, doc); + + jest + .spyOn(appContext.tshd, 'createGateway') + .mockReturnValueOnce( + new MockedUnaryCall(undefined, new Error('Something went wrong')) + ) + .mockReturnValueOnce(new MockedUnaryCall(gateway)); + + render( + + + + + + ); + + expect( + await screen.findByText('Could not establish the connection') + ).toBeInTheDocument(); + + await user.click(screen.getByText('Reconnect')); + + // Verify that the gateway was created by checking if the terminal was rendered. + // "Terminal input" is an ARIA label added by xterm.js. + expect(await screen.findByLabelText('Terminal input')).toBeInTheDocument(); +}); diff --git a/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx index 8d75b876e389f..fc3ec8ce81388 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx @@ -28,7 +28,7 @@ import * as types from 'teleterm/ui/services/workspacesService'; import { routing } from 'teleterm/ui/uri'; import { retryWithRelogin } from 'teleterm/ui/utils'; -import { OfflineGateway } from '../components/OfflineGateway'; +import { emptyFormSchema, OfflineGateway } from '../components/OfflineGateway'; /** * DocumentGatewayKube creates a terminal session that presets KUBECONFIG env @@ -54,10 +54,9 @@ export const DocumentGatewayKube = (props: { const ctx = useAppContext(); const { documentsService } = useWorkspaceContext(); const { params } = routing.parseKubeUri(doc.targetUri); - const gateway = ctx.clustersService.findGatewayByConnectionParams( - doc.targetUri, - '' - ); + const gateway = ctx.clustersService.findGatewayByConnectionParams({ + targetUri: doc.targetUri, + }); const connected = !!gateway; const [connectAttempt, createGateway] = useAsync(async () => { @@ -96,8 +95,8 @@ export const DocumentGatewayKube = (props: { connectAttempt={connectAttempt} targetName={params.kubeId} gatewayKind="kube" + formSchema={emptyFormSchema} reconnect={createGateway} - gatewayPort={{ isSupported: false }} /> ); diff --git a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts index 1245045473f0d..3b9e60c124db0 100644 --- a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts +++ b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts @@ -416,10 +416,10 @@ function createCmd( } if (doc.kind === 'doc.gateway_cli_client') { - const gateway = clustersService.findGatewayByConnectionParams( - doc.targetUri, - doc.targetUser - ); + const gateway = clustersService.findGatewayByConnectionParams({ + targetUri: doc.targetUri, + targetUser: doc.targetUser, + }); if (!gateway) { // This shouldn't happen as DocumentGatewayCliClient doesn't render DocumentTerminal before // the gateway is found. In any case, if it does happen for some reason, the user will see @@ -449,10 +449,9 @@ function createCmd( } if (doc.kind === 'doc.gateway_kube') { - const gateway = clustersService.findGatewayByConnectionParams( - doc.targetUri, - '' - ); + const gateway = clustersService.findGatewayByConnectionParams({ + targetUri: doc.targetUri, + }); if (!gateway) { throw new Error(`No gateway found for ${doc.targetUri}`); } diff --git a/web/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx b/web/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx index ce65290c2eb1f..b8fe467178b54 100644 --- a/web/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx +++ b/web/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx @@ -55,6 +55,7 @@ function getMockDocuments(): Document[] { targetUri: '/clusters/bar/dbs/foobar', targetName: 'foobar', targetUser: 'foo', + targetSubresourceName: undefined, origin: 'resource_table', status: '', }, @@ -66,6 +67,7 @@ function getMockDocuments(): Document[] { targetUri: '/clusters/bar/dbs/foobar', targetName: 'foobar', targetUser: 'bar', + targetSubresourceName: undefined, origin: 'resource_table', status: '', }, diff --git a/web/packages/teleterm/src/ui/components/FieldInputs.tsx b/web/packages/teleterm/src/ui/components/FieldInputs.tsx index 21086d8f9bb23..7e7d57e4ec40f 100644 --- a/web/packages/teleterm/src/ui/components/FieldInputs.tsx +++ b/web/packages/teleterm/src/ui/components/FieldInputs.tsx @@ -16,23 +16,26 @@ * along with this program. If not, see . */ -import { forwardRef } from 'react'; +import styled from 'styled-components'; -import FieldInput, { FieldInputProps } from 'shared/components/FieldInput'; +import FieldInput from 'shared/components/FieldInput'; -export const ConfigFieldInput = forwardRef( - (props, ref) => -); +export const ConfigFieldInput = styled(FieldInput).attrs({ size: 'small' })` + input { + &:invalid, + &:invalid:hover { + border-color: ${props => + props.theme.colors.interactive.solid.danger.default}; + } + } +`; -export const PortFieldInput = forwardRef( - (props, ref) => ( - - ) -); +export const PortFieldInput = styled(ConfigFieldInput).attrs({ + type: 'number', + min: 1, + max: 65535, + // Without a min width, the stepper controls end up being to close to a long port number such + // as 65535. minWidth instead of width allows the field to grow with the label, so that e.g. + // a custom label of "Local Port (optional)" is displayed on a single line. + minWidth: '110px', +})``; diff --git a/web/packages/teleterm/src/ui/components/OfflineGateway.tsx b/web/packages/teleterm/src/ui/components/OfflineGateway.tsx index 500a85951ba9a..1518154d5c239 100644 --- a/web/packages/teleterm/src/ui/components/OfflineGateway.tsx +++ b/web/packages/teleterm/src/ui/components/OfflineGateway.tsx @@ -16,39 +16,72 @@ * along with this program. If not, see . */ -import { useState } from 'react'; +import { FormEvent, ReactNode, useState } from 'react'; +import { z } from 'zod'; import { ButtonPrimary, Flex, H2, Text } from 'design'; import * as Alerts from 'design/Alert'; import Validation from 'shared/components/Validation'; import { Attempt } from 'shared/hooks/useAsync'; -import { PortFieldInput } from './FieldInputs'; +import { useLogger } from 'teleterm/ui/hooks/useLogger'; -export function OfflineGateway(props: { +export function OfflineGateway< + FormFieldsT extends Partial>, +>(props: { connectAttempt: Attempt; - /** Setting `isSupported` to false hides the port input. */ - gatewayPort: - | { isSupported: true; defaultPort: string } - | { isSupported: false }; - reconnect(port?: string): void; + reconnect(args: FormFieldsT): void; /** Gateway target displayed in the UI, for example, 'cockroachdb'. */ targetName: string; /** Gateway kind displayed in the UI, for example, 'database'. */ gatewayKind: string; + /** + * Each callsite is expected to pass its own formSchema that parses form data from controls passed + * through renderFormControls. If the callsite doesn't pass any form data, it's expected to use + * emptyFormSchema. We cannot do params.formSchema || emptyFormSchema, as that would mess with + * type inference. + */ + formSchema: z.ZodType; + /** + * renderFormControls allows each consumer to provide its own form fields with specific HTML form + * validation rules. The form fields are read through FormData – names on the inputs must match + * names available through the FormFields enum. + */ + renderFormControls?: (isProcessing: boolean) => ReactNode; }) { - const defaultPort = props.gatewayPort.isSupported - ? props.gatewayPort.defaultPort - : undefined; + const logger = useLogger('OfflineGateway'); + const { reconnect } = props; - const [port, setPort] = useState(defaultPort); const [reconnectRequested, setReconnectRequested] = useState(false); + const [parseError, setParseError] = useState(''); const isProcessing = props.connectAttempt.status === 'processing'; const statusDescription = isProcessing ? 'being set up…' : 'offline.'; const shouldShowReconnectControls = props.connectAttempt.status === 'error' || reconnectRequested; + const submitForm = (event: FormEvent) => { + event.preventDefault(); + setReconnectRequested(true); + setParseError(''); + + const formData = new FormData(event.currentTarget); + const parseResult = props.formSchema.safeParse( + Object.fromEntries(formData.entries()) + ); + + // Explicitly compare to false to make type inference work since strictNullChecks are off. + if (parseResult.success === false) { + // There's no need to show validation errors in the UI, since they come from a programmer + // error, not user inputting actually invalid data. + logger.error('Could not parse form', parseResult.error); + setParseError(`Could not submit form. See logs for more details.`); + return; + } + + reconnect(parseResult.data); + }; + return ( )} + {!!parseError && ( + + Form validation error + + )} { - e.preventDefault(); - setReconnectRequested(true); - props.reconnect(props.gatewayPort.isSupported ? port : undefined); - }} + onSubmit={submitForm} alignItems="flex-end" flexWrap="wrap" justifyContent="space-between" @@ -85,16 +119,11 @@ export function OfflineGateway(props: { > {shouldShowReconnectControls && ( <> - {props.gatewayPort.isSupported && ( - - setPort(e.target.value)} - /> - + {props.renderFormControls && ( + // Form controls are expected to use HTML validation instead of our Validation, but + // PortFieldInput is written in a way where it expects the context provided by + // Validation to be present, no matter whether it's used or not. + {props.renderFormControls(isProcessing)} )} Reconnect @@ -105,3 +134,15 @@ export function OfflineGateway(props: { ); } + +export enum FormFields { + LocalPort = 'localPort', + TargetSubresourceName = 'targetSubresourceName', +} +type FormFieldNames = `${FormFields}`; + +/** + * emptyFormSchema is useful in situations where the callsite that uses OfflineGateway has no form + * fields to show. + */ +export const emptyFormSchema = z.object({}); diff --git a/web/packages/teleterm/src/ui/fixtures/mocks.ts b/web/packages/teleterm/src/ui/fixtures/mocks.ts index e2c6d9d0a6985..08a15915d9b1a 100644 --- a/web/packages/teleterm/src/ui/fixtures/mocks.ts +++ b/web/packages/teleterm/src/ui/fixtures/mocks.ts @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb'; + import { MockMainProcessClient } from 'teleterm/mainProcess/fixtures/mocks'; import { MockPtyServiceClient } from 'teleterm/services/pty/fixtures/mocks'; import { @@ -24,6 +26,7 @@ import { } from 'teleterm/services/tshd/fixtures/mocks'; import { RuntimeSettings } from 'teleterm/types'; import AppContext from 'teleterm/ui/appContext'; +import { Document } from 'teleterm/ui/services/workspacesService'; export class MockAppContext extends AppContext { constructor(runtimeSettings?: Partial) { @@ -41,4 +44,23 @@ export class MockAppContext extends AppContext { getPathForFile: () => '', }); } + + addRootClusterWithDoc(cluster: Cluster, doc: Document | undefined) { + this.clustersService.setState(draftState => { + draftState.clusters.set(cluster.uri, cluster); + }); + this.workspacesService.setState(draftState => { + draftState.rootClusterUri = cluster.uri; + draftState.workspaces[cluster.uri] = { + documents: [doc].filter(Boolean), + location: doc?.uri, + localClusterUri: cluster.uri, + accessRequests: undefined, + }; + }); + } + + addRootCluster(cluster: Cluster) { + this.addRootClusterWithDoc(cluster, undefined); + } } diff --git a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts index bdd07e57cd7b5..aa33da6a1247d 100644 --- a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -39,6 +39,7 @@ import { type CloneableAbortSignal, type TshdClient, } from 'teleterm/services/tshd'; +import { getGatewayTargetUriKind } from 'teleterm/services/tshd/gateway'; import { AssumedRequest } from 'teleterm/services/tshd/types'; import { NotificationsService } from 'teleterm/ui/services/notifications'; import { UsageService } from 'teleterm/ui/services/usage'; @@ -561,7 +562,7 @@ export class ClustersService extends ImmutableStore // since we will no longer have to support old kube connections. // See call in `trackedConnectionOperationsFactory.ts` for more details. async removeKubeGateway(kubeUri: uri.KubeUri) { - const gateway = this.findGatewayByConnectionParams(kubeUri, ''); + const gateway = this.findGatewayByConnectionParams({ targetUri: kubeUri }); if (gateway) { await this.removeGateway(gateway.uri); } @@ -613,23 +614,44 @@ export class ClustersService extends ImmutableStore return this.state.gateways.get(gatewayUri); } - findGatewayByConnectionParams( - targetUri: uri.GatewayTargetUri, - targetUser: string - ) { - let found: Gateway; + findGatewayByConnectionParams({ + targetUri, + targetUser, + targetSubresourceName, + }: { + targetUri: uri.GatewayTargetUri; + targetUser?: string; + targetSubresourceName?: string; + }): Gateway | undefined { + const targetKind = getGatewayTargetUriKind(targetUri); + + for (const gateway of this.state.gateways.values()) { + if (gateway.targetUri !== targetUri) { + continue; + } - for (const [, gateway] of this.state.gateways) { - if ( - gateway.targetUri === targetUri && - gateway.targetUser === targetUser - ) { - found = gateway; - break; + switch (targetKind) { + case 'db': { + if (gateway.targetUser === targetUser) { + return gateway; + } + break; + } + case 'kube': { + // Kube gateways match only on targetUri. + return gateway; + } + case 'app': { + if (gateway.targetSubresourceName === targetSubresourceName) { + return gateway; + } + break; + } + default: { + targetKind satisfies never; + } } } - - return found; } /** diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.legacy.test.ts similarity index 98% rename from web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts rename to web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.legacy.test.ts index 1b465499c8902..c7a4c9c0795e4 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.legacy.test.ts @@ -43,6 +43,8 @@ afterEach(() => { jest.restoreAllMocks(); }); +// TODO(ravicious): Rewrite those tests to use MockAppContext instead of manually mocking everything. + it('removeItemsBelongingToRootCluster removes connections', () => { jest.mock('../workspacesService'); @@ -75,7 +77,6 @@ it('removeItemsBelongingToRootCluster removes connections', () => { targetUser: 'alice', targetName: 'test', targetSubresourceName: 'pg', - gatewayUri: '/gateways/4f68927b-579c-47a8-b965-efa8159203c9', }, { kind: 'connection.kube', diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.tsx b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.tsx new file mode 100644 index 0000000000000..f7963e45059b8 --- /dev/null +++ b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.tsx @@ -0,0 +1,255 @@ +/** + * Teleport + * Copyright (C) 2023 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 . + */ + +import 'jest-canvas-mock'; + +import { within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { act, ComponentType, createRef } from 'react'; + +import { render, screen } from 'design/utils/testing'; +import { App } from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb'; + +import Logger, { NullService } from 'teleterm/logger'; +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; +import { + makeApp, + makeAppGateway, + makeRootCluster, +} from 'teleterm/services/tshd/testHelpers'; +import { ResourcesContextProvider } from 'teleterm/ui/DocumentCluster/resourcesContext'; +import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; +import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; +import { DocumentsService } from 'teleterm/ui/services/workspacesService'; +import { TabHost } from 'teleterm/ui/TabHost'; +import { IAppContext } from 'teleterm/ui/types'; +import { unique } from 'teleterm/ui/utils'; + +import { TrackedGatewayConnection } from './types'; + +beforeAll(() => { + Logger.init(new NullService()); +}); + +test('updating target port creates new connection', async () => { + const user = userEvent.setup(); + const { ctx, docsService, app, Component } = setupTests(); + + const doc1 = docsService.createGatewayDocument({ + targetName: app.name, + targetUri: app.uri, + targetUser: undefined, + targetSubresourceName: '1337', + origin: 'resource_table', + }); + // Add without opening. It's not necessary and it'll be easier to verify activating connections + // later if we don't open the doc at this point. + docsService.add(doc1); + + render(); + + // Wait for the gateway to be created. + expect(await screen.findByText('Close Connection')).toBeInTheDocument(); + expect(ctx.connectionTracker.getConnections()).toHaveLength(1); + const conn1337 = ctx.connectionTracker.getConnections()[0]; + expect(conn1337.title).toEqual(`${app.name}:1337`); + + // Update target port. + let targetPortInput = screen.getByLabelText('Target Port'); + await user.clear(targetPortInput); + await user.type(targetPortInput, '4242'); + // We have to lose focus of that field, otherwise React is going to warn about updates not wrapped + // in act when the focus on the page changes after opening a new doc. + await user.tab(); + expect( + await screen.findByTitle('Target Port successfully updated', undefined, { + // There's a 1s debounce on port fields. + timeout: 2000, + }) + ).toBeInTheDocument(); + + // Verify connections. + expect(ctx.connectionTracker.getConnections()).toHaveLength(2); + const conn4242 = ctx.connectionTracker.getConnections()[1]; + expect(conn4242.id).not.toEqual(conn1337.id); + expect(conn4242.title).toEqual(`${app.name}:4242`); + + await act(async () => { + await ctx.connectionTracker.activateItem(conn4242.id, { + origin: 'resource_table', + }); + }); + expect(docsService.getLocation()).toEqual(doc1.uri); + + await act(async () => { + await ctx.connectionTracker.activateItem(conn1337.id, { + origin: 'resource_table', + }); + }); + expect(docsService.getDocuments()).toHaveLength(2); + expect(docsService.getLocation()).not.toEqual(doc1.uri); +}); + +test('updating target port to match connection params of gateway created by other doc is possible', async () => { + const user = userEvent.setup(); + const { ctx, docsService, app, Component } = setupTests(); + + const baseDocumentGatewayFields = { + targetName: app.name, + targetUri: app.uri, + targetUser: undefined, + origin: 'resource_table' as const, + }; + const doc1 = docsService.createGatewayDocument({ + ...baseDocumentGatewayFields, + targetSubresourceName: '1337', + }); + // Add without opening. It's not necessary and it'll be easier to verify activating connections + // later if we don't open the doc at this point. + docsService.add(doc1); + + render(); + + // Wait for the gateways to be created. + expect(await screen.findByText('Close Connection')).toBeInTheDocument(); + expect(ctx.connectionTracker.getConnections()).toHaveLength(1); + const conn1337Id = ctx.connectionTracker.getConnections()[0].id; + + // Create a second gateway. + const doc2 = docsService.createGatewayDocument({ + ...baseDocumentGatewayFields, + targetSubresourceName: '4242', + }); + await act(async () => { + docsService.add(doc2); + }); + const doc2Node = await screen.findByTestId(doc2.uri); + expect( + await within(doc2Node).findByText('Close Connection') + ).toBeInTheDocument(); + expect(ctx.connectionTracker.getConnections()).toHaveLength(2); + const conn4242Id = ctx.connectionTracker.getConnections()[1].id; + expect(conn4242Id).not.toEqual(conn1337Id); + + // Close the second gateway. + await user.click(within(doc2Node).getByText('Close Connection')); + expect(ctx.connectionTracker.findConnection(conn4242Id).connected).toBe( + false + ); + expect(ctx.connectionTracker.findConnection(conn1337Id).connected).toBe(true); + + // Update target port from 1337 to 4242. + let targetPortInput = screen.getByLabelText('Target Port'); + await user.clear(targetPortInput); + await user.type(targetPortInput, '4242'); + await user.tab(); + expect( + await screen.findByTitle('Target Port successfully updated', undefined, { + // There's a 1s debounce on port fields. + timeout: 2000, + }) + ).toBeInTheDocument(); + + // Verify that connection for 4242 is now connected and the connection for 1337 went offline. + expect(ctx.connectionTracker.getConnections()).toHaveLength(2); + const conn4242 = ctx.connectionTracker.findConnection( + conn4242Id + ) as TrackedGatewayConnection; + const conn1337 = ctx.connectionTracker.findConnection( + conn1337Id + ) as TrackedGatewayConnection; + expect(conn4242.connected).toBe(true); + expect(conn1337.connected).toBe(false); + // The ports are expected to be the same. We just changed doc with port 1337 to port 4242, so the + // corresponding connection has changed from conn1337 to conn4242. conn4242 got updated with the + // port set on doc1. + expect(conn4242).toBeTruthy(); + expect(conn4242.port).toEqual(conn1337.port); + + await act(async () => { + await ctx.connectionTracker.activateItem(conn4242Id, { + origin: 'resource_table', + }); + }); + expect(docsService.getLocation()).toEqual(doc1.uri); +}); + +function setupTests(): { + ctx: IAppContext; + docsService: DocumentsService; + app: App; + Component: ComponentType; +} { + const ctx = new MockAppContext(); + const rootCluster = makeRootCluster(); + ctx.addRootCluster(rootCluster); + ctx.workspacesService.setState(draft => { + draft.rootClusterUri = rootCluster.uri; + }); + + const docsService = ctx.workspacesService.getWorkspaceDocumentService( + rootCluster.uri + ); + + const app = makeApp({ + tcpPorts: [ + { port: 1337, endPort: 0 }, + { port: 4242, endPort: 0 }, + ], + endpointUri: 'tcp://localhost', + }); + + let gatewayLocalPort = 0; + jest.spyOn(ctx.tshd, 'createGateway').mockImplementation(async req => { + gatewayLocalPort++; + + return new MockedUnaryCall( + makeAppGateway({ + ...req, + protocol: 'TCP', + uri: `/gateways/${unique()}`, + localPort: req.localPort || gatewayLocalPort.toString(), + }) + ); + }); + jest + .spyOn(ctx.tshd, 'setGatewayTargetSubresourceName') + .mockImplementation(async req => { + const gateway = ctx.clustersService.findGateway(req.gatewayUri); + const updatedGateway = { + ...gateway, + targetSubresourceName: req.targetSubresourceName, + }; + + return new MockedUnaryCall(updatedGateway); + }); + jest + .spyOn(ctx.tshd, 'getApp') + .mockResolvedValue(new MockedUnaryCall({ app })); + + const ref = createRef(); + const Component = () => ( + + + + + + ); + + return { ctx, docsService, app, Component }; +} diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts index 8e382f8a6299a..e444834106036 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts @@ -101,6 +101,10 @@ export class ConnectionTrackerService extends ImmutableStore c.id === id); + } + findConnectionByDocument(document: Document): TrackedConnection { switch (document.kind) { case 'doc.terminal_tsh_node': @@ -199,14 +203,17 @@ export class ConnectionTrackerService extends ImmutableStore { switch (i.kind) { case 'connection.gateway': { - i.connected = !!this._clusterService.findGateway(i.gatewayUri); + i.connected = !!this._clusterService.findGatewayByConnectionParams({ + targetUri: i.targetUri, + targetUser: i.targetUser, + targetSubresourceName: i.targetSubresourceName, + }); break; } case 'connection.kube': { - i.connected = !!this._clusterService.findGatewayByConnectionParams( - i.kubeUri, - '' - ); + i.connected = !!this._clusterService.findGatewayByConnectionParams({ + targetUri: i.kubeUri, + }); break; } default: { @@ -245,9 +252,11 @@ export class ConnectionTrackerService extends ImmutableStore { let gwDoc = documentsService .getDocuments() - .find(getGatewayDocumentByConnection(connection)); + .find(getGatewayDocumentByConnection(connection)) as DocumentGateway; if (!gwDoc) { + const gw = this._clustersService.findGatewayByConnectionParams({ + targetUri: connection.targetUri, + targetUser: connection.targetUser, + targetSubresourceName: connection.targetSubresourceName, + }); + gwDoc = documentsService.createGatewayDocument({ targetUri: connection.targetUri, targetName: connection.targetName, targetUser: connection.targetUser, targetSubresourceName: connection.targetSubresourceName, title: connection.title, - gatewayUri: connection.gatewayUri, + // If the doc was closed but the gateway is still running, it's important for the + // doc to reopen with the existing gateway URI. Otherwise the doc would attempt to + // create a new gateway with the same connection params. + gatewayUri: gw?.uri, port: connection.port, origin: params.origin, }); @@ -139,16 +149,22 @@ export class TrackedConnectionOperationsFactory { documentsService.open(gwDoc.uri); }, disconnect: async () => { - return this._clustersService - .removeGateway(connection.gatewayUri) - .then(() => { - documentsService - .getDocuments() - .filter(getGatewayDocumentByConnection(connection)) - .forEach(document => { - documentsService.close(document.uri); - }); - }); + // When disconnecting, assume that the gateway exists. If a gateway doesn't exist, the UI is + // supposed to expose the remove operation, not the disconnect operation. + const gw = this._clustersService.findGatewayByConnectionParams({ + targetUri: connection.targetUri, + targetUser: connection.targetUser, + targetSubresourceName: connection.targetSubresourceName, + }); + + return this._clustersService.removeGateway(gw.uri).then(() => { + documentsService + .getDocuments() + .filter(getGatewayDocumentByConnection(connection)) + .forEach(document => { + documentsService.close(document.uri); + }); + }); }, remove: async () => {}, }; diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts b/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts index 7f724d3f38321..2fe91129e37cc 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts @@ -17,26 +17,65 @@ */ import { + Document, DocumentGateway, DocumentGatewayKube, DocumentTshKube, DocumentTshNode, DocumentTshNodeWithServerId, + getDocumentGatewayTargetUriKind, isDocumentTshNodeWithServerId, } from 'teleterm/ui/services/workspacesService'; import { unique } from 'teleterm/ui/utils/uid'; import { + TrackedConnection, TrackedGatewayConnection, TrackedKubeConnection, TrackedServerConnection, } from './types'; -export function getGatewayConnectionByDocument(document: DocumentGateway) { - return (i: TrackedGatewayConnection) => - i.kind === 'connection.gateway' && - i.targetUri === document.targetUri && - i.targetUser === document.targetUser; +/* + * Getting a connection by a document. + */ + +/** + * + * getGatewayConnectionByDocument looks for a connection that has the same gateway params as the + * document. + * + * --- + * + * This function is used in two scenarios. It's used when recreating the list of connections based + * on open documents. If there's no connection found that matches DocumentGateway, a new connection + * is added to the list. + * + * It's also used when opening new gateways for databases and apps to find an existing connection + * and call it's `activate` handler, which is going to open an existing document. If no existing + * connection is found, a new document is added to the workspace. + */ +export function getGatewayConnectionByDocument( + document: DocumentGateway +): (c: TrackedConnection) => boolean { + const targetKind = getDocumentGatewayTargetUriKind(document.targetUri); + + switch (targetKind) { + case 'db': { + return c => + c.kind === 'connection.gateway' && + c.targetUri === document.targetUri && + c.targetUser === document.targetUser; + } + case 'app': { + return c => + c.kind === 'connection.gateway' && + c.targetUri === document.targetUri && + c.targetSubresourceName === document.targetSubresourceName; + } + default: { + targetKind satisfies never; + } + } } export function getServerConnectionByDocument(document: DocumentTshNode) { @@ -60,13 +99,49 @@ export function getGatewayKubeConnectionByDocument( i.kind === 'connection.kube' && i.kubeUri === document.targetUri; } +/* + * Getting a document by a connection. + */ + +/** + * getGatewayDocumentByConnection looks for a DocumentGateway that has the same gateway params as + * the connection. + * + * --- + * + * This function is used in two scenarios. It's used when activating (clicking) a connection in the + * connections list to find a document to open if there's already a gateway for the given connection. + * + * The `activate` handler is also called when the user attempts to open a gateway for a database or + * an app. That UI action first prepares a doc with provided gateway parameters. If there's a + * connection which matches the gateway parameters from the doc (getGatewayConnectionByDocument), + * its `activate` handler is called. + * + * The second scenario is when disconnecting a connection from the connections list to find a + * document which should be closed. + */ export function getGatewayDocumentByConnection( connection: TrackedGatewayConnection -) { - return (i: DocumentGateway) => - i.kind === 'doc.gateway' && - i.targetUri === connection.targetUri && - i.targetUser === connection.targetUser; +): (d: Document) => boolean { + const targetKind = getDocumentGatewayTargetUriKind(connection.targetUri); + + switch (targetKind) { + case 'db': { + return d => + d.kind === 'doc.gateway' && + d.targetUri === connection.targetUri && + d.targetUser === connection.targetUser; + } + case 'app': { + return d => + d.kind === 'doc.gateway' && + d.targetUri === connection.targetUri && + d.targetSubresourceName === connection.targetSubresourceName; + } + default: { + targetKind satisfies never; + } + } } export function getGatewayKubeDocumentByConnection( @@ -105,7 +180,6 @@ export function createGatewayConnection( targetUser: document.targetUser, targetName: document.targetName, targetSubresourceName: document.targetSubresourceName, - gatewayUri: document.gatewayUri, }; } diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/types.ts b/web/packages/teleterm/src/ui/services/connectionTracker/types.ts index 8534f67ca72e2..a43af4c9b36c5 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/types.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/types.ts @@ -16,13 +16,7 @@ * along with this program. If not, see . */ -import { - AppUri, - DatabaseUri, - GatewayUri, - KubeUri, - ServerUri, -} from 'teleterm/ui/uri'; +import { AppUri, DatabaseUri, KubeUri, ServerUri } from 'teleterm/ui/uri'; type TrackedConnectionBase = { connected: boolean; @@ -43,7 +37,6 @@ export interface TrackedGatewayConnection extends TrackedConnectionBase { targetName: string; targetUser?: string; port?: string; - gatewayUri: GatewayUri; targetSubresourceName?: string; } diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.test.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.test.ts index ef432d0b037a8..542a0b7bf2f35 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.test.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.test.ts @@ -120,22 +120,34 @@ describe('connectToApp', () => { describe('setUpAppGateway', () => { test.each([ { - name: 'creates tunnel for a tcp app', + name: 'creates tunnel for a single-port TCP app', app: makeApp({ endpointUri: 'tcp://localhost:3000', }), }, + { + name: 'creates tunnel for a multi-port TCP app', + app: makeApp({ + endpointUri: 'tcp://localhost', + tcpPorts: [{ port: 1234, endPort: 0 }], + }), + targetPort: 1234, + expectedTitle: 'foo:1234', + }, { name: 'creates tunnel for a web app', app: makeApp({ endpointUri: 'http://localhost:3000', }), }, - ])('$name', async ({ app }) => { + ])('$name', async ({ app, targetPort, expectedTitle }) => { const appContext = new MockAppContext(); setTestCluster(appContext); - await setUpAppGateway(appContext, app, { origin: 'resource_table' }); + await setUpAppGateway(appContext, app.uri, { + telemetry: { origin: 'resource_table' }, + targetPort, + }); const documents = appContext.workspacesService .getActiveWorkspaceDocumentService() .getGatewayDocuments(); @@ -147,10 +159,10 @@ describe('setUpAppGateway', () => { port: undefined, status: '', targetName: 'foo', - targetSubresourceName: undefined, + targetSubresourceName: targetPort?.toString(), targetUri: '/clusters/teleport-local/apps/foo', targetUser: '', - title: 'foo', + title: expectedTitle || 'foo', uri: expect.any(String), }); }); diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts index 93aee047a7341..71fe3ce14d597 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts @@ -25,7 +25,7 @@ import { isWebApp, } from 'teleterm/services/tshd/app'; import { IAppContext } from 'teleterm/ui/types'; -import { routing } from 'teleterm/ui/uri'; +import { AppUri, routing } from 'teleterm/ui/uri'; import { DocumentOrigin } from './types'; @@ -115,30 +115,43 @@ export async function connectToApp( return; } - await setUpAppGateway(ctx, target, telemetry); + let targetPort: number; + if (target.tcpPorts.length > 0) { + targetPort = target.tcpPorts[0].port; + } + + await setUpAppGateway(ctx, target.uri, { telemetry, targetPort }); } export async function setUpAppGateway( ctx: IAppContext, - target: App, - telemetry: { origin: DocumentOrigin } + targetUri: AppUri, + options: { + telemetry: { origin: DocumentOrigin }; + /** + * targetPort allows the caller to preselect the target port for the gateway. Should be passed + * only for multi-port TCP apps. + */ + targetPort?: number; + } ) { - const rootClusterUri = routing.ensureRootClusterUri(target.uri); + const rootClusterUri = routing.ensureRootClusterUri(targetUri); const documentsService = ctx.workspacesService.getWorkspaceDocumentService(rootClusterUri); const doc = documentsService.createGatewayDocument({ - targetUri: target.uri, - origin: telemetry.origin, - targetName: routing.parseAppUri(target.uri).params.appId, + targetUri: targetUri, + origin: options.telemetry.origin, + targetName: routing.parseAppUri(targetUri).params.appId, targetUser: '', + targetSubresourceName: options.targetPort?.toString(), }); const connectionToReuse = ctx.connectionTracker.findConnectionByDocument(doc); if (connectionToReuse) { await ctx.connectionTracker.activateItem(connectionToReuse.id, { - origin: telemetry.origin, + origin: options.telemetry.origin, }); } else { await ctx.workspacesService.setActiveWorkspace(rootClusterUri); diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts index b50989a4273ff..96d1f3129ea24 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts @@ -79,6 +79,7 @@ describe('document should be added', () => { targetUri: '/clusters/bar/dbs/quux', targetName: 'quux', targetUser: 'foo', + targetSubresourceName: undefined, origin: 'resource_table', status: '', }; @@ -155,6 +156,7 @@ test('only gateway documents should be returned', () => { targetUri: '/clusters/bar/dbs/quux', targetName: 'quux', targetUser: 'foo', + targetSubresourceName: undefined, origin: 'resource_table', status: '', }; diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts index 79420a6cab0a1..cd7350996ae9a 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts @@ -29,6 +29,7 @@ import { } from 'teleterm/ui/uri'; import { unique } from 'teleterm/ui/utils/uid'; +import { getDocumentGatewayTitle } from './documentsUtils'; import { CreateAccessRequestDocumentOpts, CreateGatewayDocumentOpts, @@ -155,9 +156,8 @@ export class DocumentsService { origin, } = opts; const uri = routing.getDocUri({ docId: unique() }); - const title = targetUser ? `${targetUser}@${targetName}` : targetName; - return { + const doc: DocumentGateway = { uri, kind: 'doc.gateway', targetUri, @@ -165,11 +165,13 @@ export class DocumentsService { targetName, targetSubresourceName, gatewayUri, - title, + title: undefined, port, origin, status: '', }; + doc.title = getDocumentGatewayTitle(doc); + return doc; } createGatewayCliDocument({ diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts index 6bd654e01d963..9eb9008aa66f3 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts @@ -16,9 +16,18 @@ * along with this program. If not, see . */ -import { ClusterOrResourceUri, routing } from 'teleterm/ui/uri'; +import { + ClusterOrResourceUri, + isAppUri, + isDatabaseUri, + routing, +} from 'teleterm/ui/uri'; -import { Document, isDocumentTshNodeWithServerId } from './types'; +import { + Document, + DocumentGateway, + isDocumentTshNodeWithServerId, +} from './types'; /** * getResourceUri returns the URI of the cluster resource that is the subject of the document. @@ -62,3 +71,44 @@ export function getResourceUri( return undefined; } } + +/** + * getDocumentGatewayTargetUriKind is used when the callsite needs to distinguish between different + * kinds of targets that DocumentGateway supports when given only its target URI. + */ +export function getDocumentGatewayTargetUriKind( + targetUri: DocumentGateway['targetUri'] +): 'db' | 'app' { + if (isDatabaseUri(targetUri)) { + return 'db'; + } + + if (isAppUri(targetUri)) { + return 'app'; + } + + // TODO(ravicious): Optimally we'd use `targetUri satisfies never` here to have a type error when + // DocumentGateway['targetUri'] is changed. + // + // However, at the moment that field is essentially of type string, so there's not much we can do + // with regards to type safety. +} + +export function getDocumentGatewayTitle(doc: DocumentGateway): string { + const { targetName, targetUri, targetUser, targetSubresourceName } = doc; + const targetKind = getDocumentGatewayTargetUriKind(targetUri); + + switch (targetKind) { + case 'db': { + return targetUser ? `${targetUser}@${targetName}` : targetName; + } + case 'app': { + return targetSubresourceName + ? `${targetName}:${targetSubresourceName}` + : targetName; + } + default: { + targetKind satisfies never; + } + } +} diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts index 970fb09d22ba7..d39f12167515d 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts @@ -101,15 +101,47 @@ export interface DocumentTshKube extends DocumentBase { origin: DocumentOrigin; } +/** + * DocumentGateway is used for database and app gateways. The two are distinguished by the kind of + * resource that targetUri points to. + */ export interface DocumentGateway extends DocumentBase { kind: 'doc.gateway'; - // status is used merely to show a progress bar when the gateway is being set up. + /** status is used merely to show a progress bar when the gateway is being set up. */ status: '' | 'connecting' | 'connected' | 'error'; + /** + * gatewayUri is not present until the gateway described by the document is created. + */ gatewayUri?: uri.GatewayUri; targetUri: uri.DatabaseUri | uri.AppUri; + /** + * targetUser is used only for db gateways and must contain the db user. Connect allows only a + * single doc.gateway to exist per targetUri + targetUser combo. + */ targetUser: string; + /** + * targetName contains the name of the target resource as shown in the UI. This field could be + * removed in favor of targetUri, which always includes the target name anyway. + */ targetName: string; - targetSubresourceName?: string; + /** + * targetSubresourceName contains database name for db gateways and target port for TCP app + * gateways. A DocumentGateway created for a multi-port TCP app is expected to always have this + * field present. + * + * For app gateways, Connect allows only a single doc.gateway to exist per targetUri + + * targetSubresourceName combo. + * + * For db gateways, targetSubresourceName is not taken into account when considering document + * "uniqueness". + */ + targetSubresourceName: string | undefined; + /** + * port is the local port on which the gateway accepts connections. + * + * If empty, tshd is going to created a listener on a random port and then this field will be + * updated to match that random port. + */ port?: string; origin: DocumentOrigin; }