mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
Connect: close cluster clients when profile changes (#61090)
* Include expiration time in `LoggedInUser` This will allow the profile watcher to detect when the user relogged. * Display expiration time in UI * Add `ClearStaleClusterClients` RPC * Implement `ClearStaleClusterClients` * Clear stale clients when profile changes * Improve session expiration component * Move refresh button back to top * `ClearCachedStaleClientsForRoot` -> `ClearStaleCachedClientsForRoot` * `unchanged` -> `stale` * Make "closing stale clients" a subtest * Add `clientcache` test * Remove `getProfile` error wrapping * Improve comment * Convert story to controls
This commit is contained in:
@@ -177,7 +177,7 @@ func (p *Profile) TLSConfig() (*tls.Config, error) {
|
||||
|
||||
// Expiry returns the credential expiry.
|
||||
func (p *Profile) Expiry() (time.Time, bool) {
|
||||
certPEMBlock, err := os.ReadFile(p.TLSCertPath())
|
||||
certPEMBlock, err := p.TLSCert()
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
@@ -188,6 +188,12 @@ func (p *Profile) Expiry() (time.Time, bool) {
|
||||
return cert.NotAfter, true
|
||||
}
|
||||
|
||||
// TLSCert returns the profile's TLS certificate.
|
||||
func (p *Profile) TLSCert() ([]byte, error) {
|
||||
certPEMBlock, err := os.ReadFile(p.TLSCertPath())
|
||||
return certPEMBlock, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// RequireKubeLocalProxy returns true if this profile indicates a local proxy
|
||||
// is required for kube access.
|
||||
func (p *Profile) RequireKubeLocalProxy() bool {
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
types "github.com/gravitational/teleport/api/types"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
@@ -328,8 +329,10 @@ type LoggedInUser struct {
|
||||
IsDeviceTrusted bool `protobuf:"varint,9,opt,name=is_device_trusted,json=isDeviceTrusted,proto3" json:"is_device_trusted,omitempty"`
|
||||
// Indicates whether access may be hindered by the lack of a trusted device.
|
||||
TrustedDeviceRequirement types.TrustedDeviceRequirement `protobuf:"varint,10,opt,name=trusted_device_requirement,json=trustedDeviceRequirement,proto3,enum=types.TrustedDeviceRequirement" json:"trusted_device_requirement,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
// Expiration time of the certificate.
|
||||
ValidUntil *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=valid_until,json=validUntil,proto3" json:"valid_until,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LoggedInUser) Reset() {
|
||||
@@ -425,6 +428,13 @@ func (x *LoggedInUser) GetTrustedDeviceRequirement() types.TrustedDeviceRequirem
|
||||
return types.TrustedDeviceRequirement(0)
|
||||
}
|
||||
|
||||
func (x *LoggedInUser) GetValidUntil() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.ValidUntil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ACL is the access control list of the user
|
||||
type ACL struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@@ -756,7 +766,7 @@ var File_teleport_lib_teleterm_v1_cluster_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_teleport_lib_teleterm_v1_cluster_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"&teleport/lib/teleterm/v1/cluster.proto\x12\x18teleport.lib.teleterm.v1\x1a6teleport/legacy/types/trusted_device_requirement.proto\"\xf8\x03\n" +
|
||||
"&teleport/lib/teleterm/v1/cluster.proto\x12\x18teleport.lib.teleterm.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a6teleport/legacy/types/trusted_device_requirement.proto\"\xf8\x03\n" +
|
||||
"\aCluster\x12\x10\n" +
|
||||
"\x03uri\x18\x01 \x01(\tR\x03uri\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1d\n" +
|
||||
@@ -771,7 +781,7 @@ const file_teleport_lib_teleterm_v1_cluster_proto_rawDesc = "" +
|
||||
" \x01(\tR\fproxyVersion\x12N\n" +
|
||||
"\x0eshow_resources\x18\v \x01(\x0e2'.teleport.lib.teleterm.v1.ShowResourcesR\rshowResources\x120\n" +
|
||||
"\x14profile_status_error\x18\f \x01(\tR\x12profileStatusError\x12\x19\n" +
|
||||
"\bsso_host\x18\r \x01(\tR\assoHost\"\xaa\x04\n" +
|
||||
"\bsso_host\x18\r \x01(\tR\assoHost\"\xe7\x04\n" +
|
||||
"\fLoggedInUser\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
|
||||
"\x05roles\x18\x02 \x03(\tR\x05roles\x12/\n" +
|
||||
@@ -782,7 +792,9 @@ const file_teleport_lib_teleterm_v1_cluster_proto_rawDesc = "" +
|
||||
"\tuser_type\x18\b \x01(\x0e2/.teleport.lib.teleterm.v1.LoggedInUser.UserTypeR\buserType\x12*\n" +
|
||||
"\x11is_device_trusted\x18\t \x01(\bR\x0fisDeviceTrusted\x12]\n" +
|
||||
"\x1atrusted_device_requirement\x18\n" +
|
||||
" \x01(\x0e2\x1f.types.TrustedDeviceRequirementR\x18trustedDeviceRequirement\"M\n" +
|
||||
" \x01(\x0e2\x1f.types.TrustedDeviceRequirementR\x18trustedDeviceRequirement\x12;\n" +
|
||||
"\vvalid_until\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\n" +
|
||||
"validUntil\"M\n" +
|
||||
"\bUserType\x12\x19\n" +
|
||||
"\x15USER_TYPE_UNSPECIFIED\x10\x00\x12\x13\n" +
|
||||
"\x0fUSER_TYPE_LOCAL\x10\x01\x12\x11\n" +
|
||||
@@ -844,6 +856,7 @@ var file_teleport_lib_teleterm_v1_cluster_proto_goTypes = []any{
|
||||
(*ResourceAccess)(nil), // 5: teleport.lib.teleterm.v1.ResourceAccess
|
||||
(*Features)(nil), // 6: teleport.lib.teleterm.v1.Features
|
||||
(types.TrustedDeviceRequirement)(0), // 7: types.TrustedDeviceRequirement
|
||||
(*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp
|
||||
}
|
||||
var file_teleport_lib_teleterm_v1_cluster_proto_depIdxs = []int32{
|
||||
3, // 0: teleport.lib.teleterm.v1.Cluster.logged_in_user:type_name -> teleport.lib.teleterm.v1.LoggedInUser
|
||||
@@ -852,24 +865,25 @@ var file_teleport_lib_teleterm_v1_cluster_proto_depIdxs = []int32{
|
||||
4, // 3: teleport.lib.teleterm.v1.LoggedInUser.acl:type_name -> teleport.lib.teleterm.v1.ACL
|
||||
1, // 4: teleport.lib.teleterm.v1.LoggedInUser.user_type:type_name -> teleport.lib.teleterm.v1.LoggedInUser.UserType
|
||||
7, // 5: teleport.lib.teleterm.v1.LoggedInUser.trusted_device_requirement:type_name -> types.TrustedDeviceRequirement
|
||||
5, // 6: teleport.lib.teleterm.v1.ACL.auth_connectors:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 7: teleport.lib.teleterm.v1.ACL.roles:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 8: teleport.lib.teleterm.v1.ACL.users:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 9: teleport.lib.teleterm.v1.ACL.trusted_clusters:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 10: teleport.lib.teleterm.v1.ACL.events:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 11: teleport.lib.teleterm.v1.ACL.tokens:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 12: teleport.lib.teleterm.v1.ACL.servers:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 13: teleport.lib.teleterm.v1.ACL.apps:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 14: teleport.lib.teleterm.v1.ACL.dbs:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 15: teleport.lib.teleterm.v1.ACL.kubeservers:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 16: teleport.lib.teleterm.v1.ACL.access_requests:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 17: teleport.lib.teleterm.v1.ACL.recorded_sessions:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 18: teleport.lib.teleterm.v1.ACL.active_sessions:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
19, // [19:19] is the sub-list for method output_type
|
||||
19, // [19:19] is the sub-list for method input_type
|
||||
19, // [19:19] is the sub-list for extension type_name
|
||||
19, // [19:19] is the sub-list for extension extendee
|
||||
0, // [0:19] is the sub-list for field type_name
|
||||
8, // 6: teleport.lib.teleterm.v1.LoggedInUser.valid_until:type_name -> google.protobuf.Timestamp
|
||||
5, // 7: teleport.lib.teleterm.v1.ACL.auth_connectors:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 8: teleport.lib.teleterm.v1.ACL.roles:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 9: teleport.lib.teleterm.v1.ACL.users:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 10: teleport.lib.teleterm.v1.ACL.trusted_clusters:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 11: teleport.lib.teleterm.v1.ACL.events:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 12: teleport.lib.teleterm.v1.ACL.tokens:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 13: teleport.lib.teleterm.v1.ACL.servers:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 14: teleport.lib.teleterm.v1.ACL.apps:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 15: teleport.lib.teleterm.v1.ACL.dbs:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 16: teleport.lib.teleterm.v1.ACL.kubeservers:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 17: teleport.lib.teleterm.v1.ACL.access_requests:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 18: teleport.lib.teleterm.v1.ACL.recorded_sessions:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
5, // 19: teleport.lib.teleterm.v1.ACL.active_sessions:type_name -> teleport.lib.teleterm.v1.ResourceAccess
|
||||
20, // [20:20] is the sub-list for method output_type
|
||||
20, // [20:20] is the sub-list for method input_type
|
||||
20, // [20:20] is the sub-list for extension type_name
|
||||
20, // [20:20] is the sub-list for extension extendee
|
||||
0, // [0:20] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_teleport_lib_teleterm_v1_cluster_proto_init() }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,7 @@ const (
|
||||
TerminalService_Login_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/Login"
|
||||
TerminalService_LoginPasswordless_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/LoginPasswordless"
|
||||
TerminalService_Logout_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/Logout"
|
||||
TerminalService_ClearStaleClusterClients_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/ClearStaleClusterClients"
|
||||
TerminalService_TransferFile_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/TransferFile"
|
||||
TerminalService_ReportUsageEvent_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/ReportUsageEvent"
|
||||
TerminalService_UpdateHeadlessAuthenticationState_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/UpdateHeadlessAuthenticationState"
|
||||
@@ -178,6 +179,8 @@ type TerminalServiceClient interface {
|
||||
// Optionally removes the profile.
|
||||
// This operation is idempotent and can be safely invoked multiple times.
|
||||
Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*EmptyResponse, error)
|
||||
// Closes root and leaf cluster clients that use outdated TLS certificates.
|
||||
ClearStaleClusterClients(ctx context.Context, in *ClearStaleClusterClientsRequest, opts ...grpc.CallOption) (*ClearStaleClusterClientsResponse, error)
|
||||
// TransferFile sends a request to download/upload a file
|
||||
TransferFile(ctx context.Context, in *FileTransferRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileTransferProgress], error)
|
||||
// ReportUsageEvent allows to send usage events that are then anonymized and forwarded to prehog
|
||||
@@ -520,6 +523,16 @@ func (c *terminalServiceClient) Logout(ctx context.Context, in *LogoutRequest, o
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *terminalServiceClient) ClearStaleClusterClients(ctx context.Context, in *ClearStaleClusterClientsRequest, opts ...grpc.CallOption) (*ClearStaleClusterClientsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ClearStaleClusterClientsResponse)
|
||||
err := c.cc.Invoke(ctx, TerminalService_ClearStaleClusterClients_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *terminalServiceClient) TransferFile(ctx context.Context, in *FileTransferRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileTransferProgress], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &TerminalService_ServiceDesc.Streams[1], TerminalService_TransferFile_FullMethodName, cOpts...)
|
||||
@@ -779,6 +792,8 @@ type TerminalServiceServer interface {
|
||||
// Optionally removes the profile.
|
||||
// This operation is idempotent and can be safely invoked multiple times.
|
||||
Logout(context.Context, *LogoutRequest) (*EmptyResponse, error)
|
||||
// Closes root and leaf cluster clients that use outdated TLS certificates.
|
||||
ClearStaleClusterClients(context.Context, *ClearStaleClusterClientsRequest) (*ClearStaleClusterClientsResponse, error)
|
||||
// TransferFile sends a request to download/upload a file
|
||||
TransferFile(*FileTransferRequest, grpc.ServerStreamingServer[FileTransferProgress]) error
|
||||
// ReportUsageEvent allows to send usage events that are then anonymized and forwarded to prehog
|
||||
@@ -922,6 +937,9 @@ func (UnimplementedTerminalServiceServer) LoginPasswordless(grpc.BidiStreamingSe
|
||||
func (UnimplementedTerminalServiceServer) Logout(context.Context, *LogoutRequest) (*EmptyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented")
|
||||
}
|
||||
func (UnimplementedTerminalServiceServer) ClearStaleClusterClients(context.Context, *ClearStaleClusterClientsRequest) (*ClearStaleClusterClientsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ClearStaleClusterClients not implemented")
|
||||
}
|
||||
func (UnimplementedTerminalServiceServer) TransferFile(*FileTransferRequest, grpc.ServerStreamingServer[FileTransferProgress]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method TransferFile not implemented")
|
||||
}
|
||||
@@ -1481,6 +1499,24 @@ func _TerminalService_Logout_Handler(srv interface{}, ctx context.Context, dec f
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _TerminalService_ClearStaleClusterClients_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ClearStaleClusterClientsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(TerminalServiceServer).ClearStaleClusterClients(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: TerminalService_ClearStaleClusterClients_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(TerminalServiceServer).ClearStaleClusterClients(ctx, req.(*ClearStaleClusterClientsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _TerminalService_TransferFile_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(FileTransferRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
@@ -1848,6 +1884,10 @@ var TerminalService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Logout",
|
||||
Handler: _TerminalService_Logout_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ClearStaleClusterClients",
|
||||
Handler: _TerminalService_ClearStaleClusterClients_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ReportUsageEvent",
|
||||
Handler: _TerminalService_ReportUsageEvent_Handler,
|
||||
|
||||
+15
-1
@@ -30,6 +30,7 @@ import { UnknownFieldHandler } from "@protobuf-ts/runtime";
|
||||
import type { PartialMessage } from "@protobuf-ts/runtime";
|
||||
import { reflectionMergePartial } from "@protobuf-ts/runtime";
|
||||
import { MessageType } from "@protobuf-ts/runtime";
|
||||
import { Timestamp } from "../../../../google/protobuf/timestamp_pb";
|
||||
import { TrustedDeviceRequirement } from "../../../legacy/types/trusted_device_requirement_pb";
|
||||
/**
|
||||
* Cluster describes cluster fields.
|
||||
@@ -191,6 +192,12 @@ export interface LoggedInUser {
|
||||
* @generated from protobuf field: types.TrustedDeviceRequirement trusted_device_requirement = 10;
|
||||
*/
|
||||
trustedDeviceRequirement: TrustedDeviceRequirement;
|
||||
/**
|
||||
* Expiration time of the certificate.
|
||||
*
|
||||
* @generated from protobuf field: google.protobuf.Timestamp valid_until = 11;
|
||||
*/
|
||||
validUntil?: Timestamp;
|
||||
}
|
||||
/**
|
||||
* UserType indicates whether the user was created through an SSO provider or in Teleport itself.
|
||||
@@ -541,7 +548,8 @@ class LoggedInUser$Type extends MessageType<LoggedInUser> {
|
||||
{ no: 7, name: "requestable_roles", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ },
|
||||
{ no: 8, name: "user_type", kind: "enum", T: () => ["teleport.lib.teleterm.v1.LoggedInUser.UserType", LoggedInUser_UserType, "USER_TYPE_"] },
|
||||
{ no: 9, name: "is_device_trusted", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
|
||||
{ no: 10, name: "trusted_device_requirement", kind: "enum", T: () => ["types.TrustedDeviceRequirement", TrustedDeviceRequirement, "TRUSTED_DEVICE_REQUIREMENT_"] }
|
||||
{ no: 10, name: "trusted_device_requirement", kind: "enum", T: () => ["types.TrustedDeviceRequirement", TrustedDeviceRequirement, "TRUSTED_DEVICE_REQUIREMENT_"] },
|
||||
{ no: 11, name: "valid_until", kind: "message", T: () => Timestamp }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<LoggedInUser>): LoggedInUser {
|
||||
@@ -590,6 +598,9 @@ class LoggedInUser$Type extends MessageType<LoggedInUser> {
|
||||
case /* types.TrustedDeviceRequirement trusted_device_requirement */ 10:
|
||||
message.trustedDeviceRequirement = reader.int32();
|
||||
break;
|
||||
case /* google.protobuf.Timestamp valid_until */ 11:
|
||||
message.validUntil = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.validUntil);
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
@@ -629,6 +640,9 @@ class LoggedInUser$Type extends MessageType<LoggedInUser> {
|
||||
/* types.TrustedDeviceRequirement trusted_device_requirement = 10; */
|
||||
if (message.trustedDeviceRequirement !== 0)
|
||||
writer.tag(10, WireType.Varint).int32(message.trustedDeviceRequirement);
|
||||
/* google.protobuf.Timestamp valid_until = 11; */
|
||||
if (message.validUntil)
|
||||
Timestamp.internalBinaryWrite(message.validUntil, writer.tag(11, WireType.LengthDelimited).fork(), options).join();
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
|
||||
+32
-15
@@ -54,6 +54,8 @@ import type { ReportUsageEventRequest } from "./usage_events_pb";
|
||||
import type { FileTransferProgress } from "./service_pb";
|
||||
import type { FileTransferRequest } from "./service_pb";
|
||||
import type { ServerStreamingCall } from "@protobuf-ts/runtime-rpc";
|
||||
import type { ClearStaleClusterClientsResponse } from "./service_pb";
|
||||
import type { ClearStaleClusterClientsRequest } from "./service_pb";
|
||||
import type { LogoutRequest } from "./service_pb";
|
||||
import type { LoginPasswordlessResponse } from "./service_pb";
|
||||
import type { LoginPasswordlessRequest } from "./service_pb";
|
||||
@@ -315,6 +317,12 @@ export interface ITerminalServiceClient {
|
||||
* @generated from protobuf rpc: Logout(teleport.lib.teleterm.v1.LogoutRequest) returns (teleport.lib.teleterm.v1.EmptyResponse);
|
||||
*/
|
||||
logout(input: LogoutRequest, options?: RpcOptions): UnaryCall<LogoutRequest, EmptyResponse>;
|
||||
/**
|
||||
* Closes root and leaf cluster clients that use outdated TLS certificates.
|
||||
*
|
||||
* @generated from protobuf rpc: ClearStaleClusterClients(teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest) returns (teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse);
|
||||
*/
|
||||
clearStaleClusterClients(input: ClearStaleClusterClientsRequest, options?: RpcOptions): UnaryCall<ClearStaleClusterClientsRequest, ClearStaleClusterClientsResponse>;
|
||||
/**
|
||||
* TransferFile sends a request to download/upload a file
|
||||
*
|
||||
@@ -723,13 +731,22 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
const method = this.methods[27], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<LogoutRequest, EmptyResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
* Closes root and leaf cluster clients that use outdated TLS certificates.
|
||||
*
|
||||
* @generated from protobuf rpc: ClearStaleClusterClients(teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest) returns (teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse);
|
||||
*/
|
||||
clearStaleClusterClients(input: ClearStaleClusterClientsRequest, options?: RpcOptions): UnaryCall<ClearStaleClusterClientsRequest, ClearStaleClusterClientsResponse> {
|
||||
const method = this.methods[28], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<ClearStaleClusterClientsRequest, ClearStaleClusterClientsResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
* TransferFile sends a request to download/upload a file
|
||||
*
|
||||
* @generated from protobuf rpc: TransferFile(teleport.lib.teleterm.v1.FileTransferRequest) returns (stream teleport.lib.teleterm.v1.FileTransferProgress);
|
||||
*/
|
||||
transferFile(input: FileTransferRequest, options?: RpcOptions): ServerStreamingCall<FileTransferRequest, FileTransferProgress> {
|
||||
const method = this.methods[28], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[29], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<FileTransferRequest, FileTransferProgress>("serverStreaming", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -738,7 +755,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: ReportUsageEvent(teleport.lib.teleterm.v1.ReportUsageEventRequest) returns (teleport.lib.teleterm.v1.EmptyResponse);
|
||||
*/
|
||||
reportUsageEvent(input: ReportUsageEventRequest, options?: RpcOptions): UnaryCall<ReportUsageEventRequest, EmptyResponse> {
|
||||
const method = this.methods[29], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[30], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<ReportUsageEventRequest, EmptyResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -748,7 +765,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: UpdateHeadlessAuthenticationState(teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest) returns (teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateResponse);
|
||||
*/
|
||||
updateHeadlessAuthenticationState(input: UpdateHeadlessAuthenticationStateRequest, options?: RpcOptions): UnaryCall<UpdateHeadlessAuthenticationStateRequest, UpdateHeadlessAuthenticationStateResponse> {
|
||||
const method = this.methods[30], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[31], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<UpdateHeadlessAuthenticationStateRequest, UpdateHeadlessAuthenticationStateResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -759,7 +776,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: CreateConnectMyComputerRole(teleport.lib.teleterm.v1.CreateConnectMyComputerRoleRequest) returns (teleport.lib.teleterm.v1.CreateConnectMyComputerRoleResponse);
|
||||
*/
|
||||
createConnectMyComputerRole(input: CreateConnectMyComputerRoleRequest, options?: RpcOptions): UnaryCall<CreateConnectMyComputerRoleRequest, CreateConnectMyComputerRoleResponse> {
|
||||
const method = this.methods[31], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[32], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<CreateConnectMyComputerRoleRequest, CreateConnectMyComputerRoleResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -768,7 +785,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: CreateConnectMyComputerNodeToken(teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenRequest) returns (teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenResponse);
|
||||
*/
|
||||
createConnectMyComputerNodeToken(input: CreateConnectMyComputerNodeTokenRequest, options?: RpcOptions): UnaryCall<CreateConnectMyComputerNodeTokenRequest, CreateConnectMyComputerNodeTokenResponse> {
|
||||
const method = this.methods[32], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[33], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<CreateConnectMyComputerNodeTokenRequest, CreateConnectMyComputerNodeTokenResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -782,7 +799,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: WaitForConnectMyComputerNodeJoin(teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinRequest) returns (teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse);
|
||||
*/
|
||||
waitForConnectMyComputerNodeJoin(input: WaitForConnectMyComputerNodeJoinRequest, options?: RpcOptions): UnaryCall<WaitForConnectMyComputerNodeJoinRequest, WaitForConnectMyComputerNodeJoinResponse> {
|
||||
const method = this.methods[33], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[34], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<WaitForConnectMyComputerNodeJoinRequest, WaitForConnectMyComputerNodeJoinResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -791,7 +808,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: DeleteConnectMyComputerNode(teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeRequest) returns (teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeResponse);
|
||||
*/
|
||||
deleteConnectMyComputerNode(input: DeleteConnectMyComputerNodeRequest, options?: RpcOptions): UnaryCall<DeleteConnectMyComputerNodeRequest, DeleteConnectMyComputerNodeResponse> {
|
||||
const method = this.methods[34], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[35], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<DeleteConnectMyComputerNodeRequest, DeleteConnectMyComputerNodeResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -800,7 +817,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: GetConnectMyComputerNodeName(teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameRequest) returns (teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameResponse);
|
||||
*/
|
||||
getConnectMyComputerNodeName(input: GetConnectMyComputerNodeNameRequest, options?: RpcOptions): UnaryCall<GetConnectMyComputerNodeNameRequest, GetConnectMyComputerNodeNameResponse> {
|
||||
const method = this.methods[35], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[36], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<GetConnectMyComputerNodeNameRequest, GetConnectMyComputerNodeNameResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -809,7 +826,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: ListUnifiedResources(teleport.lib.teleterm.v1.ListUnifiedResourcesRequest) returns (teleport.lib.teleterm.v1.ListUnifiedResourcesResponse);
|
||||
*/
|
||||
listUnifiedResources(input: ListUnifiedResourcesRequest, options?: RpcOptions): UnaryCall<ListUnifiedResourcesRequest, ListUnifiedResourcesResponse> {
|
||||
const method = this.methods[36], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[37], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<ListUnifiedResourcesRequest, ListUnifiedResourcesResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -818,7 +835,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: GetUserPreferences(teleport.lib.teleterm.v1.GetUserPreferencesRequest) returns (teleport.lib.teleterm.v1.GetUserPreferencesResponse);
|
||||
*/
|
||||
getUserPreferences(input: GetUserPreferencesRequest, options?: RpcOptions): UnaryCall<GetUserPreferencesRequest, GetUserPreferencesResponse> {
|
||||
const method = this.methods[37], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[38], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<GetUserPreferencesRequest, GetUserPreferencesResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -828,7 +845,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: UpdateUserPreferences(teleport.lib.teleterm.v1.UpdateUserPreferencesRequest) returns (teleport.lib.teleterm.v1.UpdateUserPreferencesResponse);
|
||||
*/
|
||||
updateUserPreferences(input: UpdateUserPreferencesRequest, options?: RpcOptions): UnaryCall<UpdateUserPreferencesRequest, UpdateUserPreferencesResponse> {
|
||||
const method = this.methods[38], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[39], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<UpdateUserPreferencesRequest, UpdateUserPreferencesResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -841,7 +858,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: AuthenticateWebDevice(teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest) returns (teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse);
|
||||
*/
|
||||
authenticateWebDevice(input: AuthenticateWebDeviceRequest, options?: RpcOptions): UnaryCall<AuthenticateWebDeviceRequest, AuthenticateWebDeviceResponse> {
|
||||
const method = this.methods[39], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[40], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<AuthenticateWebDeviceRequest, AuthenticateWebDeviceResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -851,7 +868,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: GetApp(teleport.lib.teleterm.v1.GetAppRequest) returns (teleport.lib.teleterm.v1.GetAppResponse);
|
||||
*/
|
||||
getApp(input: GetAppRequest, options?: RpcOptions): UnaryCall<GetAppRequest, GetAppResponse> {
|
||||
const method = this.methods[40], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[41], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<GetAppRequest, GetAppResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
@@ -860,7 +877,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: ConnectToDesktop(stream teleport.lib.teleterm.v1.ConnectToDesktopRequest) returns (stream teleport.lib.teleterm.v1.ConnectToDesktopResponse);
|
||||
*/
|
||||
connectToDesktop(options?: RpcOptions): DuplexStreamingCall<ConnectToDesktopRequest, ConnectToDesktopResponse> {
|
||||
const method = this.methods[41], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[42], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<ConnectToDesktopRequest, ConnectToDesktopResponse>("duplex", this._transport, method, opt);
|
||||
}
|
||||
/**
|
||||
@@ -874,7 +891,7 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf
|
||||
* @generated from protobuf rpc: SetSharedDirectoryForDesktopSession(teleport.lib.teleterm.v1.SetSharedDirectoryForDesktopSessionRequest) returns (teleport.lib.teleterm.v1.SetSharedDirectoryForDesktopSessionResponse);
|
||||
*/
|
||||
setSharedDirectoryForDesktopSession(input: SetSharedDirectoryForDesktopSessionRequest, options?: RpcOptions): UnaryCall<SetSharedDirectoryForDesktopSessionRequest, SetSharedDirectoryForDesktopSessionResponse> {
|
||||
const method = this.methods[42], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[43], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<SetSharedDirectoryForDesktopSessionRequest, SetSharedDirectoryForDesktopSessionResponse>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ import { UpdateHeadlessAuthenticationStateRequest } from "./service_pb";
|
||||
import { ReportUsageEventRequest } from "./usage_events_pb";
|
||||
import { FileTransferProgress } from "./service_pb";
|
||||
import { FileTransferRequest } from "./service_pb";
|
||||
import { ClearStaleClusterClientsResponse } from "./service_pb";
|
||||
import { ClearStaleClusterClientsRequest } from "./service_pb";
|
||||
import { LogoutRequest } from "./service_pb";
|
||||
import { LoginPasswordlessResponse } from "./service_pb";
|
||||
import { LoginPasswordlessRequest } from "./service_pb";
|
||||
@@ -308,6 +310,12 @@ export interface ITerminalService extends grpc.UntypedServiceImplementation {
|
||||
* @generated from protobuf rpc: Logout(teleport.lib.teleterm.v1.LogoutRequest) returns (teleport.lib.teleterm.v1.EmptyResponse);
|
||||
*/
|
||||
logout: grpc.handleUnaryCall<LogoutRequest, EmptyResponse>;
|
||||
/**
|
||||
* Closes root and leaf cluster clients that use outdated TLS certificates.
|
||||
*
|
||||
* @generated from protobuf rpc: ClearStaleClusterClients(teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest) returns (teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse);
|
||||
*/
|
||||
clearStaleClusterClients: grpc.handleUnaryCall<ClearStaleClusterClientsRequest, ClearStaleClusterClientsResponse>;
|
||||
/**
|
||||
* TransferFile sends a request to download/upload a file
|
||||
*
|
||||
@@ -710,6 +718,16 @@ export const terminalServiceDefinition: grpc.ServiceDefinition<ITerminalService>
|
||||
responseSerialize: value => Buffer.from(EmptyResponse.toBinary(value)),
|
||||
requestSerialize: value => Buffer.from(LogoutRequest.toBinary(value))
|
||||
},
|
||||
clearStaleClusterClients: {
|
||||
path: "/teleport.lib.teleterm.v1.TerminalService/ClearStaleClusterClients",
|
||||
originalName: "ClearStaleClusterClients",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
responseDeserialize: bytes => ClearStaleClusterClientsResponse.fromBinary(bytes),
|
||||
requestDeserialize: bytes => ClearStaleClusterClientsRequest.fromBinary(bytes),
|
||||
responseSerialize: value => Buffer.from(ClearStaleClusterClientsResponse.toBinary(value)),
|
||||
requestSerialize: value => Buffer.from(ClearStaleClusterClientsRequest.toBinary(value))
|
||||
},
|
||||
transferFile: {
|
||||
path: "/teleport.lib.teleterm.v1.TerminalService/TransferFile",
|
||||
originalName: "TransferFile",
|
||||
|
||||
+87
@@ -82,6 +82,20 @@ export interface LogoutRequest {
|
||||
*/
|
||||
removeProfile: boolean;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest
|
||||
*/
|
||||
export interface ClearStaleClusterClientsRequest {
|
||||
/**
|
||||
* @generated from protobuf field: string root_cluster_uri = 1;
|
||||
*/
|
||||
rootClusterUri: string;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse
|
||||
*/
|
||||
export interface ClearStaleClusterClientsResponse {
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message teleport.lib.teleterm.v1.StartHeadlessWatcherRequest
|
||||
*/
|
||||
@@ -1537,6 +1551,78 @@ class LogoutRequest$Type extends MessageType<LogoutRequest> {
|
||||
*/
|
||||
export const LogoutRequest = new LogoutRequest$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class ClearStaleClusterClientsRequest$Type extends MessageType<ClearStaleClusterClientsRequest> {
|
||||
constructor() {
|
||||
super("teleport.lib.teleterm.v1.ClearStaleClusterClientsRequest", [
|
||||
{ no: 1, name: "root_cluster_uri", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<ClearStaleClusterClientsRequest>): ClearStaleClusterClientsRequest {
|
||||
const message = globalThis.Object.create((this.messagePrototype!));
|
||||
message.rootClusterUri = "";
|
||||
if (value !== undefined)
|
||||
reflectionMergePartial<ClearStaleClusterClientsRequest>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ClearStaleClusterClientsRequest): ClearStaleClusterClientsRequest {
|
||||
let message = target ?? this.create(), end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* string root_cluster_uri */ 1:
|
||||
message.rootClusterUri = 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: ClearStaleClusterClientsRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||
/* string root_cluster_uri = 1; */
|
||||
if (message.rootClusterUri !== "")
|
||||
writer.tag(1, WireType.LengthDelimited).string(message.rootClusterUri);
|
||||
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.ClearStaleClusterClientsRequest
|
||||
*/
|
||||
export const ClearStaleClusterClientsRequest = new ClearStaleClusterClientsRequest$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class ClearStaleClusterClientsResponse$Type extends MessageType<ClearStaleClusterClientsResponse> {
|
||||
constructor() {
|
||||
super("teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse", []);
|
||||
}
|
||||
create(value?: PartialMessage<ClearStaleClusterClientsResponse>): ClearStaleClusterClientsResponse {
|
||||
const message = globalThis.Object.create((this.messagePrototype!));
|
||||
if (value !== undefined)
|
||||
reflectionMergePartial<ClearStaleClusterClientsResponse>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ClearStaleClusterClientsResponse): ClearStaleClusterClientsResponse {
|
||||
return target ?? this.create();
|
||||
}
|
||||
internalBinaryWrite(message: ClearStaleClusterClientsResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message teleport.lib.teleterm.v1.ClearStaleClusterClientsResponse
|
||||
*/
|
||||
export const ClearStaleClusterClientsResponse = new ClearStaleClusterClientsResponse$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class StartHeadlessWatcherRequest$Type extends MessageType<StartHeadlessWatcherRequest> {
|
||||
constructor() {
|
||||
super("teleport.lib.teleterm.v1.StartHeadlessWatcherRequest", [
|
||||
@@ -5879,6 +5965,7 @@ export const TerminalService = new ServiceType("teleport.lib.teleterm.v1.Termina
|
||||
{ name: "Login", options: {}, I: LoginRequest, O: EmptyResponse },
|
||||
{ name: "LoginPasswordless", serverStreaming: true, clientStreaming: true, options: {}, I: LoginPasswordlessRequest, O: LoginPasswordlessResponse },
|
||||
{ name: "Logout", options: {}, I: LogoutRequest, O: EmptyResponse },
|
||||
{ name: "ClearStaleClusterClients", options: {}, I: ClearStaleClusterClientsRequest, O: ClearStaleClusterClientsResponse },
|
||||
{ name: "TransferFile", serverStreaming: true, options: {}, I: FileTransferRequest, O: FileTransferProgress },
|
||||
{ name: "ReportUsageEvent", options: {}, I: ReportUsageEventRequest, O: EmptyResponse },
|
||||
{ name: "UpdateHeadlessAuthenticationState", options: {}, I: UpdateHeadlessAuthenticationStateRequest, O: UpdateHeadlessAuthenticationStateResponse },
|
||||
|
||||
@@ -134,6 +134,12 @@ func TestTeleterm(t *testing.T) {
|
||||
testClientCache(t, pack, creds)
|
||||
})
|
||||
|
||||
t.Run("clearing stale cached clients", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testClearingStaleCachedClients(t, pack, creds)
|
||||
})
|
||||
|
||||
t.Run("logging out", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testLogout(t, pack, creds)
|
||||
@@ -554,6 +560,62 @@ func testClientCache(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers.
|
||||
require.NotEqual(t, secondCallForClient, thirdCallForClient)
|
||||
}
|
||||
|
||||
func testClearingStaleCachedClients(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers.UserCreds) {
|
||||
ctx := context.Background()
|
||||
|
||||
tc := mustLogin(t, pack.Root.User.GetName(), pack, creds)
|
||||
|
||||
storageFakeClock := clockwork.NewFakeClockAt(time.Now())
|
||||
|
||||
storage, err := clusters.NewStorage(clusters.Config{
|
||||
ClientStore: tc.ClientStore,
|
||||
Clock: storageFakeClock,
|
||||
InsecureSkipVerify: tc.InsecureSkipVerify,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
cluster, _, err := storage.Add(ctx, tc.WebProxyAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
tshdEventsClient := daemon.NewTshdEventsClient(func() (grpc.DialOption, error) {
|
||||
return grpc.WithTransportCredentials(insecure.NewCredentials()), nil
|
||||
})
|
||||
|
||||
daemonService, err := daemon.New(daemon.Config{
|
||||
Storage: storage,
|
||||
TshdEventsClient: tshdEventsClient,
|
||||
KubeconfigsDir: t.TempDir(),
|
||||
AgentsDir: t.TempDir(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
daemonService.Stop()
|
||||
})
|
||||
|
||||
firstCallForClient, err := daemonService.GetCachedClient(ctx, cluster.URI)
|
||||
require.NoError(t, err)
|
||||
err = daemonService.ClearStaleCachedClientsForRoot(cluster.URI)
|
||||
require.NoError(t, err)
|
||||
// Ensure the client wasn't closed.
|
||||
secondCallForClient, err := daemonService.GetCachedClient(ctx, cluster.URI)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, firstCallForClient, secondCallForClient)
|
||||
// Reissue user certs by assuming a role with a bogus ID in DropAccessRequests.
|
||||
accessRequest := &api.AssumeRoleRequest{
|
||||
RootClusterUri: cluster.URI.String(),
|
||||
DropRequestIds: []string{"does-not-matter"},
|
||||
}
|
||||
err = cluster.AssumeRole(ctx, firstCallForClient, accessRequest)
|
||||
require.NoError(t, err)
|
||||
// The cert has changed, so after clearing stale clients,
|
||||
// GetCachedClient should return a new client.
|
||||
err = daemonService.ClearStaleCachedClientsForRoot(cluster.URI)
|
||||
require.NoError(t, err)
|
||||
thirdCallForClient, err := daemonService.GetCachedClient(ctx, cluster.URI)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, secondCallForClient, thirdCallForClient)
|
||||
}
|
||||
|
||||
func testLogout(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers.UserCreds) {
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package clientcache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log/slog"
|
||||
"slices"
|
||||
@@ -35,12 +36,47 @@ import (
|
||||
type Cache struct {
|
||||
cfg Config
|
||||
mu sync.RWMutex
|
||||
// clients keeps a mapping from key (profile name and leaf cluster name) to cluster client.
|
||||
clients map[key]*client.ClusterClient
|
||||
// clients keeps a mapping from key (profile name and leaf cluster name) to client.
|
||||
clients map[key]*clientWithMetadata
|
||||
// group prevents duplicate requests to create clients for a given cluster.
|
||||
group singleflight.Group
|
||||
}
|
||||
|
||||
type clientWithMetadata struct {
|
||||
client *client.ClusterClient
|
||||
// tlsCert is the certificate that was loaded when the client was created.
|
||||
tlsCert []byte
|
||||
// getProfile reads the fresh profile for the client from disk.
|
||||
getProfile func() (profile, error)
|
||||
}
|
||||
|
||||
type profile interface {
|
||||
// TLSCert returns the current TLS cert, stored in the agent.
|
||||
TLSCert() ([]byte, error)
|
||||
}
|
||||
|
||||
// isTLSCertStale checks if the cached client uses the current TLS cert
|
||||
// (read from the agent). If not, the TLS cert is considered stale.
|
||||
func (c *clientWithMetadata) isTLSCertStale() (bool, error) {
|
||||
pr, err := c.getProfile()
|
||||
if err != nil {
|
||||
if trace.IsNotFound(err) {
|
||||
return true, nil
|
||||
}
|
||||
return false, trace.Wrap(err)
|
||||
}
|
||||
|
||||
tlsCert, err := pr.TLSCert()
|
||||
if err != nil {
|
||||
if trace.IsNotFound(err) {
|
||||
return true, nil
|
||||
}
|
||||
return false, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return !bytes.Equal(c.tlsCert, tlsCert), nil
|
||||
}
|
||||
|
||||
// NewClientFunc is a function that will return a new [*client.TeleportClient] for a given profile and leaf
|
||||
// cluster. [leafClusterName] may be empty, in which case implementations should return a client for the root cluster.
|
||||
type NewClientFunc func(ctx context.Context, profileName, leafClusterName string) (*client.TeleportClient, error)
|
||||
@@ -89,7 +125,7 @@ func New(c Config) (*Cache, error) {
|
||||
|
||||
return &Cache{
|
||||
cfg: c,
|
||||
clients: make(map[key]*client.ClusterClient),
|
||||
clients: make(map[key]*clientWithMetadata),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -100,7 +136,7 @@ func (c *Cache) Get(ctx context.Context, profileName, leafClusterName string) (*
|
||||
groupClt, err, _ := c.group.Do(k.String(), func() (any, error) {
|
||||
if fromCache := c.getFromCache(k); fromCache != nil {
|
||||
c.cfg.Logger.DebugContext(ctx, "Retrieved client from cache", "cluster", k)
|
||||
return fromCache, nil
|
||||
return fromCache.client, nil
|
||||
}
|
||||
|
||||
tc, err := c.cfg.NewClientFunc(ctx, profileName, leafClusterName)
|
||||
@@ -120,8 +156,19 @@ func (c *Cache) Get(ctx context.Context, profileName, leafClusterName string) (*
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
keyRing, err := tc.LocalAgent().GetCoreKeyRing()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Save the client in the cache, so we don't have to build a new connection next time.
|
||||
c.addToCache(k, newClient)
|
||||
c.addToCache(k, &clientWithMetadata{
|
||||
client: newClient,
|
||||
tlsCert: keyRing.TLSCert,
|
||||
getProfile: func() (profile, error) {
|
||||
return tc.GetProfile(tc.WebProxyAddr)
|
||||
},
|
||||
})
|
||||
|
||||
c.cfg.Logger.InfoContext(ctx, "Added client to cache", "cluster", k)
|
||||
|
||||
@@ -139,8 +186,25 @@ func (c *Cache) Get(ctx context.Context, profileName, leafClusterName string) (*
|
||||
return clt, nil
|
||||
}
|
||||
|
||||
// ClearOption configures ClearForRoot behavior.
|
||||
type ClearOption func(*clearConfig)
|
||||
|
||||
type clearConfig struct {
|
||||
onlyClearClientsWithStaleCert bool
|
||||
}
|
||||
|
||||
// WithClearingOnlyClientsWithStaleCert closes only clients that use outdated certs.
|
||||
func WithClearingOnlyClientsWithStaleCert() ClearOption {
|
||||
return func(c *clearConfig) { c.onlyClearClientsWithStaleCert = true }
|
||||
}
|
||||
|
||||
// ClearForRoot closes and removes clients from the cache for the root cluster and its leaf clusters.
|
||||
func (c *Cache) ClearForRoot(profileName string) error {
|
||||
func (c *Cache) ClearForRoot(profileName string, opts ...ClearOption) error {
|
||||
cfg := &clearConfig{}
|
||||
for _, o := range opts {
|
||||
o(cfg)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -150,13 +214,23 @@ func (c *Cache) ClearForRoot(profileName string) error {
|
||||
)
|
||||
|
||||
for k, clt := range c.clients {
|
||||
if k.profile == profileName {
|
||||
if err := clt.Close(); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
deleted = append(deleted, k.String())
|
||||
delete(c.clients, k)
|
||||
if k.profile != profileName {
|
||||
continue
|
||||
}
|
||||
if cfg.onlyClearClientsWithStaleCert {
|
||||
stale, err := clt.isTLSCertStale()
|
||||
// If an error occurs, close the client as well.
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
} else if !stale {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := clt.client.Close(); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
deleted = append(deleted, k.String())
|
||||
delete(c.clients, k)
|
||||
}
|
||||
|
||||
c.cfg.Logger.InfoContext(context.Background(), "Invalidated cached clients for root cluster",
|
||||
@@ -165,7 +239,6 @@ func (c *Cache) ClearForRoot(profileName string) error {
|
||||
)
|
||||
|
||||
return trace.NewAggregate(errors...)
|
||||
|
||||
}
|
||||
|
||||
// Clear closes and removes all clients.
|
||||
@@ -175,7 +248,7 @@ func (c *Cache) Clear() error {
|
||||
|
||||
var errors []error
|
||||
for _, clt := range c.clients {
|
||||
if err := clt.Close(); err != nil {
|
||||
if err := clt.client.Close(); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
@@ -184,14 +257,14 @@ func (c *Cache) Clear() error {
|
||||
return trace.NewAggregate(errors...)
|
||||
}
|
||||
|
||||
func (c *Cache) addToCache(k key, clusterClient *client.ClusterClient) {
|
||||
func (c *Cache) addToCache(k key, clt *clientWithMetadata) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.clients[k] = clusterClient
|
||||
c.clients[k] = clt
|
||||
}
|
||||
|
||||
func (c *Cache) getFromCache(k key) *client.ClusterClient {
|
||||
func (c *Cache) getFromCache(k key) *clientWithMetadata {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
@@ -241,7 +314,7 @@ func (c *NoCache) Get(ctx context.Context, profileName, leafClusterName string)
|
||||
return newClient, nil
|
||||
}
|
||||
|
||||
func (c *NoCache) ClearForRoot(profileName string) error {
|
||||
func (c *NoCache) ClearForRoot(profileName string, _ ...ClearOption) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package clientcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
apiprofile "github.com/gravitational/teleport/api/profile"
|
||||
"github.com/gravitational/teleport/api/utils/keys"
|
||||
"github.com/gravitational/teleport/lib/auth/testauthority"
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/fixtures"
|
||||
"github.com/gravitational/teleport/lib/observability/tracing"
|
||||
"github.com/gravitational/teleport/lib/sshca"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
)
|
||||
|
||||
func TestClearingClientsWithStaleCert(t *testing.T) {
|
||||
privateKey, err := keys.ParsePrivateKey(fixtures.PEMBytes["rsa"])
|
||||
require.NoError(t, err)
|
||||
tlsCert, sshCert, err := makeCerts(privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyRing := client.NewKeyRing(privateKey, privateKey)
|
||||
keyRing.KeyRingIndex = client.KeyRingIndex{
|
||||
ProxyHost: "localhost",
|
||||
Username: "testuser",
|
||||
ClusterName: "root",
|
||||
}
|
||||
keyRing.Cert = sshCert
|
||||
keyRing.TLSCert = tlsCert
|
||||
|
||||
profile := &apiprofile.Profile{
|
||||
WebProxyAddr: keyRing.ProxyHost,
|
||||
Username: keyRing.Username,
|
||||
SiteName: keyRing.ClusterName,
|
||||
}
|
||||
clientStore := client.NewFSClientStore(t.TempDir())
|
||||
err = clientStore.SaveProfile(profile, true)
|
||||
require.NoError(t, err)
|
||||
err = clientStore.AddKeyRing(keyRing)
|
||||
require.NoError(t, err)
|
||||
|
||||
cache, err := New(Config{
|
||||
NewClientFunc: func(ctx context.Context, profileName, leafClusterName string) (*client.TeleportClient, error) {
|
||||
config := &client.Config{
|
||||
ClientStore: clientStore,
|
||||
SSHProxyAddr: "localhost:3080",
|
||||
WebProxyAddr: "localhost:3080",
|
||||
Username: "testuser",
|
||||
Tracer: tracing.NoopProvider().Tracer("test"),
|
||||
SiteName: "root",
|
||||
}
|
||||
if leafClusterName != "" {
|
||||
config.SiteName = leafClusterName
|
||||
}
|
||||
tc, err := client.NewClient(config)
|
||||
|
||||
return tc, err
|
||||
},
|
||||
RetryWithReloginFunc: func(ctx context.Context, tc *client.TeleportClient, fn func() error, opts ...client.RetryWithReloginOption) error {
|
||||
return fn()
|
||||
},
|
||||
Logger: slog.New(slog.DiscardHandler),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get clients.
|
||||
rootClient, err := cache.Get(t.Context(), "root", "")
|
||||
require.NoError(t, err)
|
||||
leaf1Client, err := cache.Get(t.Context(), "root", "leaf1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update the TLS cert.
|
||||
tlsCert, _, err = makeCerts(privateKey)
|
||||
require.NoError(t, err)
|
||||
keyRing.TLSCert = tlsCert
|
||||
err = clientStore.AddKeyRing(keyRing)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the client for a new leaf after the cert has been updated.
|
||||
leaf2Client, err := cache.Get(t.Context(), "root", "leaf2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Clear stale clients.
|
||||
err = cache.ClearForRoot("root", WithClearingOnlyClientsWithStaleCert())
|
||||
require.NoError(t, err)
|
||||
|
||||
newRootClient, err := cache.Get(t.Context(), "root", "")
|
||||
require.NoError(t, err)
|
||||
newLeaf1Client, err := cache.Get(t.Context(), "root", "leaf1")
|
||||
require.NoError(t, err)
|
||||
newLeaf2Client, err := cache.Get(t.Context(), "root", "leaf2")
|
||||
require.NoError(t, err)
|
||||
// Clients opened before updating the cert should be reopened.
|
||||
require.NotEqual(t, newRootClient, rootClient)
|
||||
require.NotEqual(t, newLeaf1Client, leaf1Client)
|
||||
// The client opened after updating the cert should be untouched.
|
||||
require.Equal(t, newLeaf2Client, leaf2Client)
|
||||
}
|
||||
|
||||
// makeCerts makes TSL and SSH certs.
|
||||
func makeCerts(privateKey *keys.PrivateKey) ([]byte, []byte, error) {
|
||||
cert, err := tlsca.GenerateSelfSignedCAWithSigner(privateKey, pkix.Name{
|
||||
CommonName: "root",
|
||||
Organization: []string{"localhost"},
|
||||
}, nil, defaults.CATTL)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
ca, err := tlsca.FromCertAndSigner(cert, privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
keygen := testauthority.New()
|
||||
|
||||
clock := clockwork.NewRealClock()
|
||||
identity := tlsca.Identity{
|
||||
Username: "testuser",
|
||||
}
|
||||
|
||||
subject, err := identity.Subject()
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
tlsCert, err := ca.GenerateCertificate(tlsca.CertificateRequest{
|
||||
Clock: clock,
|
||||
PublicKey: privateKey.Public(),
|
||||
Subject: subject,
|
||||
NotAfter: clock.Now().UTC().Add(defaults.CATTL),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
signer, err := keys.ParsePrivateKey([]byte(fixtures.SSHCAPrivateKey))
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
caSigner, err := ssh.NewSignerFromKey(signer)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
sshCert, err := keygen.GenerateUserCert(sshca.UserCertificateRequest{
|
||||
CASigner: caSigner,
|
||||
PublicUserKey: ssh.MarshalAuthorizedKey(privateKey.SSHPublicKey()),
|
||||
Identity: sshca.Identity{
|
||||
Username: "testuser",
|
||||
Principals: []string{"testuser"},
|
||||
},
|
||||
})
|
||||
|
||||
return tlsCert, sshCert, trace.Wrap(err)
|
||||
}
|
||||
@@ -22,9 +22,11 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -82,6 +84,17 @@ func (s *Handler) GetCluster(ctx context.Context, req *api.GetClusterRequest) (*
|
||||
return apiRootClusterWithDetails, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// ClearStaleClusterClients closes root and leaf cluster clients that use outdated TLS certificates.
|
||||
func (s *Handler) ClearStaleClusterClients(_ context.Context, req *api.ClearStaleClusterClientsRequest) (*api.ClearStaleClusterClientsResponse, error) {
|
||||
parsed, err := uri.Parse(req.RootClusterUri)
|
||||
if err != nil {
|
||||
return &api.ClearStaleClusterClientsResponse{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
err = s.DaemonService.ClearStaleCachedClientsForRoot(parsed)
|
||||
return &api.ClearStaleClusterClientsResponse{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
func newAPIRootCluster(cluster *clusters.Cluster) *api.Cluster {
|
||||
loggedInUser := cluster.GetLoggedInUser()
|
||||
|
||||
@@ -95,6 +108,7 @@ func newAPIRootCluster(cluster *clusters.Cluster) *api.Cluster {
|
||||
Roles: loggedInUser.Roles,
|
||||
ActiveRequests: loggedInUser.ActiveRequests,
|
||||
IsDeviceTrusted: cluster.HasDeviceTrustExtensions(),
|
||||
ValidUntil: timestamppb.New(loggedInUser.ValidUntil),
|
||||
},
|
||||
SsoHost: cluster.SSOHost,
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ package clusters
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/jonboulle/clockwork"
|
||||
@@ -312,6 +313,7 @@ func (c *Cluster) GetLoggedInUser() LoggedInUser {
|
||||
SSHLogins: c.status.Logins,
|
||||
Roles: c.status.Roles,
|
||||
ActiveRequests: c.status.ActiveRequests,
|
||||
ValidUntil: c.status.ValidUntil,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,6 +355,8 @@ type LoggedInUser struct {
|
||||
Roles []string
|
||||
// ActiveRequests is the user active requests
|
||||
ActiveRequests []string
|
||||
// ValidUntil is expiration time of the certificate.
|
||||
ValidUntil time.Time
|
||||
}
|
||||
|
||||
// AddMetadataToRetryableError is Connect's equivalent of client.RetryWithRelogin. By adding the
|
||||
|
||||
@@ -95,7 +95,7 @@ type ClientCache interface {
|
||||
Get(ctx context.Context, profileName, leafClusterName string) (*client.ClusterClient, error)
|
||||
// ClearForRoot closes and removes clients from the cache
|
||||
// for the root cluster and its leaf clusters.
|
||||
ClearForRoot(profileName string) error
|
||||
ClearForRoot(profileName string, opts ...clientcache.ClearOption) error
|
||||
// Clear closes and removes all clients.
|
||||
Clear() error
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1"
|
||||
"github.com/gravitational/teleport/lib/auth/authclient"
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/client/clientcache"
|
||||
"github.com/gravitational/teleport/lib/client/sso"
|
||||
dtauthn "github.com/gravitational/teleport/lib/devicetrust/authn"
|
||||
"github.com/gravitational/teleport/lib/teleterm/api/uri"
|
||||
@@ -1283,6 +1284,14 @@ func (s *Service) ClearCachedClientsForRoot(clusterURI uri.ResourceURI) error {
|
||||
return trace.Wrap(s.clientCache.ClearForRoot(profileName))
|
||||
}
|
||||
|
||||
// ClearStaleCachedClientsForRoot closes and removes clients from the cache
|
||||
// for the root cluster and its leaf clusters, if their cert is outdated.
|
||||
func (s *Service) ClearStaleCachedClientsForRoot(clusterURI uri.ResourceURI) error {
|
||||
profileName := clusterURI.GetProfileName()
|
||||
err := s.clientCache.ClearForRoot(profileName, clientcache.WithClearingOnlyClientsWithStaleCert())
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// SetSharedDirectoryForDesktopSession opens a directory for a desktop session and enables file system operations for it.
|
||||
// If there is no active desktop session associated with the specified desktop_uri and login,
|
||||
// an error is returned.
|
||||
|
||||
@@ -20,6 +20,7 @@ syntax = "proto3";
|
||||
|
||||
package teleport.lib.teleterm.v1;
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "teleport/legacy/types/trusted_device_requirement.proto";
|
||||
|
||||
option go_package = "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1;teletermv1";
|
||||
@@ -111,6 +112,8 @@ message LoggedInUser {
|
||||
bool is_device_trusted = 9;
|
||||
// Indicates whether access may be hindered by the lack of a trusted device.
|
||||
types.TrustedDeviceRequirement trusted_device_requirement = 10;
|
||||
// Expiration time of the certificate.
|
||||
google.protobuf.Timestamp valid_until = 11;
|
||||
}
|
||||
|
||||
// ACL is the access control list of the user
|
||||
|
||||
@@ -135,6 +135,8 @@ service TerminalService {
|
||||
// Optionally removes the profile.
|
||||
// This operation is idempotent and can be safely invoked multiple times.
|
||||
rpc Logout(LogoutRequest) returns (EmptyResponse);
|
||||
// Closes root and leaf cluster clients that use outdated TLS certificates.
|
||||
rpc ClearStaleClusterClients(ClearStaleClusterClientsRequest) returns (ClearStaleClusterClientsResponse);
|
||||
// TransferFile sends a request to download/upload a file
|
||||
rpc TransferFile(FileTransferRequest) returns (stream FileTransferProgress);
|
||||
// ReportUsageEvent allows to send usage events that are then anonymized and forwarded to prehog
|
||||
@@ -204,6 +206,12 @@ message LogoutRequest {
|
||||
bool remove_profile = 2;
|
||||
}
|
||||
|
||||
message ClearStaleClusterClientsRequest {
|
||||
string root_cluster_uri = 1;
|
||||
}
|
||||
|
||||
message ClearStaleClusterClientsResponse {}
|
||||
|
||||
message StartHeadlessWatcherRequest {
|
||||
string root_cluster_uri = 1;
|
||||
}
|
||||
|
||||
+6
@@ -209,6 +209,12 @@ export class ClusterLifecycleManager {
|
||||
if (hasLoggedOut) {
|
||||
await this.handleClusterLogout(next);
|
||||
} else {
|
||||
const client = await this.getTshdClient();
|
||||
// Only clear clients with outdated certificates.
|
||||
// The watcher 'changed' event may be emitted right after the user logs in
|
||||
// or assumes a role via Connect (which already closes all clients
|
||||
// for the profile), so we avoid closing them again if they're already up to date.
|
||||
await client.clearStaleClusterClients({ rootClusterUri: next.uri });
|
||||
await this.syncOrUpdateCluster(next);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ export function mergeClusterProfileWithDetails({
|
||||
activeRequests: profile.loggedInUser.activeRequests,
|
||||
roles: profile.loggedInUser.roles,
|
||||
isDeviceTrusted: profile.loggedInUser.isDeviceTrusted,
|
||||
validUntil: profile.loggedInUser.validUntil,
|
||||
userType:
|
||||
details.loggedInUser?.userType || LoggedInUser_UserType.UNSPECIFIED,
|
||||
trustedDeviceRequirement:
|
||||
|
||||
@@ -74,6 +74,7 @@ export class MockTshClient implements TshdClient {
|
||||
login = () => new MockedUnaryCall({});
|
||||
loginPasswordless = undefined;
|
||||
logout = () => new MockedUnaryCall({});
|
||||
clearStaleClusterClients = () => new MockedUnaryCall({});
|
||||
transferFile = undefined;
|
||||
reportUsageEvent = () => new MockedUnaryCall({});
|
||||
createConnectMyComputerRole = () =>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
|
||||
import { TrustedDeviceRequirement } from 'gen-proto-ts/teleport/legacy/types/trusted_device_requirement_pb';
|
||||
import { App } from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb';
|
||||
import {
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
} from 'gen-proto-ts/teleport/lib/teleterm/v1/auth_settings_pb';
|
||||
import {
|
||||
ACL,
|
||||
LoggedInUser_UserType,
|
||||
ShowResources,
|
||||
} from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
||||
import { WindowsDesktop } from 'gen-proto-ts/teleport/lib/teleterm/v1/windows_desktop_pb';
|
||||
@@ -259,7 +261,8 @@ export const makeLoggedInUser = (
|
||||
roles: [],
|
||||
requestableRoles: [],
|
||||
suggestedReviewers: [],
|
||||
userType: tsh.LoggedInUser_UserType.LOCAL,
|
||||
userType: LoggedInUser_UserType.LOCAL,
|
||||
validUntil: Timestamp.fromDate(new Date()),
|
||||
...props,
|
||||
});
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
import Flex from 'design/Flex';
|
||||
import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
|
||||
import { TrustedDeviceRequirement } from 'gen-proto-ts/teleport/legacy/types/trusted_device_requirement_pb';
|
||||
import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
||||
|
||||
@@ -32,21 +34,122 @@ import { RootClusterUri } from 'teleterm/ui/uri';
|
||||
|
||||
import { IdentityContainer } from './Identity';
|
||||
|
||||
export default {
|
||||
interface StoryProps {
|
||||
clusters: ('violet' | 'orange' | 'green')[];
|
||||
activeCluster: boolean;
|
||||
activeClusterExpired: boolean;
|
||||
deviceTrust: 'enrolled' | 'required-not-enrolled' | 'not-enrolled';
|
||||
showProfileErrors: boolean;
|
||||
}
|
||||
|
||||
const meta: Meta<StoryProps> = {
|
||||
title: 'Teleterm/Identity',
|
||||
component: props => {
|
||||
const hasOrange = props.clusters.includes('orange');
|
||||
const hasViolet = props.clusters.includes('violet');
|
||||
const hasGreen = props.clusters.includes('green');
|
||||
const clusters = [
|
||||
hasOrange &&
|
||||
makeRootCluster({
|
||||
...clusterOrange,
|
||||
profileStatusError: props.showProfileErrors ? profileStatusError : '',
|
||||
}),
|
||||
hasViolet &&
|
||||
makeRootCluster({
|
||||
...clusterViolet,
|
||||
profileStatusError: props.showProfileErrors ? profileStatusError : '',
|
||||
}),
|
||||
hasGreen &&
|
||||
makeRootCluster({
|
||||
...clusterGreen,
|
||||
profileStatusError: props.showProfileErrors ? profileStatusError : '',
|
||||
}),
|
||||
].filter(Boolean);
|
||||
|
||||
const hasClusterWithLoggedInUser =
|
||||
props.activeCluster && (hasOrange || hasViolet);
|
||||
if (hasClusterWithLoggedInUser) {
|
||||
clusters[0].loggedInUser = makeLoggedInUser({
|
||||
...clusters[0].loggedInUser,
|
||||
validUntil: Timestamp.fromDate(
|
||||
props.activeClusterExpired
|
||||
? new Date()
|
||||
: new Date(Date.now() + 24 * 60 * 60 * 1000)
|
||||
),
|
||||
isDeviceTrusted: props.deviceTrust === 'enrolled',
|
||||
trustedDeviceRequirement:
|
||||
props.deviceTrust === 'required-not-enrolled'
|
||||
? TrustedDeviceRequirement.REQUIRED
|
||||
: TrustedDeviceRequirement.NOT_REQUIRED,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<OpenIdentityPopover
|
||||
clusters={clusters}
|
||||
activeClusterUri={hasClusterWithLoggedInUser && clusters[0]?.uri}
|
||||
/>
|
||||
);
|
||||
},
|
||||
argTypes: {
|
||||
clusters: {
|
||||
control: { type: 'check' },
|
||||
options: ['violet', 'orange', 'green'],
|
||||
description: 'List of clusters to show.',
|
||||
},
|
||||
activeCluster: {
|
||||
control: { type: 'boolean' },
|
||||
description: 'Makes "violet" or "orange" an active cluster.',
|
||||
},
|
||||
deviceTrust: {
|
||||
control: { type: 'radio' },
|
||||
options: ['enrolled', 'required-not-enrolled', 'not-enrolled'],
|
||||
description: 'Controls device trust requirement.',
|
||||
},
|
||||
activeClusterExpired: {
|
||||
control: { type: 'boolean' },
|
||||
description: 'Whether the active cluster has expired cert.',
|
||||
},
|
||||
showProfileErrors: {
|
||||
control: { type: 'boolean' },
|
||||
description: 'Shows profile errors for all clusters.',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
clusters: ['violet', 'orange', 'green'],
|
||||
activeCluster: true,
|
||||
deviceTrust: 'not-enrolled',
|
||||
activeClusterExpired: false,
|
||||
showProfileErrors: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
const clusterOrange = makeRootCluster({
|
||||
name: 'orange',
|
||||
name: 'orange-psv-eindhoven-eredivisie-production-lorem-ipsum',
|
||||
loggedInUser: makeLoggedInUser({
|
||||
name: 'bob',
|
||||
roles: ['access', 'editor'],
|
||||
name: 'ruud-van-nistelrooy-van-der-sar',
|
||||
roles: [
|
||||
'circle-mark-app-access',
|
||||
'grafana-lite-app-access',
|
||||
'grafana-gold-app-access',
|
||||
'release-lion-app-access',
|
||||
'release-fox-app-access',
|
||||
'sales-center-lorem-app-access',
|
||||
'sales-center-ipsum-db-access',
|
||||
'sales-center-shop-app-access',
|
||||
'sales-center-floor-db-access',
|
||||
],
|
||||
}),
|
||||
uri: '/clusters/orange',
|
||||
});
|
||||
const clusterViolet = makeRootCluster({
|
||||
name: 'violet',
|
||||
loggedInUser: makeLoggedInUser({ name: 'sammy' }),
|
||||
loggedInUser: makeLoggedInUser({
|
||||
name: 'sammy',
|
||||
roles: ['access', 'editor'],
|
||||
}),
|
||||
uri: '/clusters/violet',
|
||||
});
|
||||
const clusterGreen = makeRootCluster({
|
||||
@@ -66,8 +169,14 @@ const OpenIdentityPopover = (props: {
|
||||
props.clusters.forEach(c => {
|
||||
ctx.addRootCluster(c);
|
||||
});
|
||||
ctx.workspacesService.addWorkspace(clusterGreen.uri);
|
||||
ctx.workspacesService.addWorkspace(clusterViolet.uri);
|
||||
ctx.workspacesService.addWorkspace(clusterOrange.uri);
|
||||
ctx.workspacesService.setState(draftState => {
|
||||
draftState.rootClusterUri = props.activeClusterUri;
|
||||
draftState.workspaces[clusterGreen.uri].color = 'green';
|
||||
draftState.workspaces[clusterViolet.uri].color = 'purple';
|
||||
draftState.workspaces[clusterOrange.uri].color = 'yellow';
|
||||
});
|
||||
useOpenPopover();
|
||||
|
||||
@@ -98,125 +207,60 @@ const useOpenPopover = () => {
|
||||
}, []);
|
||||
};
|
||||
|
||||
export function NoRootClusters() {
|
||||
return <OpenIdentityPopover clusters={[]} activeClusterUri={undefined} />;
|
||||
}
|
||||
export const NoRootClusters: StoryObj<StoryProps> = {
|
||||
args: {
|
||||
clusters: [],
|
||||
},
|
||||
};
|
||||
|
||||
export function OneClusterWithNoActiveCluster() {
|
||||
return (
|
||||
<OpenIdentityPopover
|
||||
activeClusterUri={undefined}
|
||||
clusters={[makeRootCluster({ loggedInUser: undefined })]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const OneClusterWithNoActiveCluster: StoryObj<StoryProps> = {
|
||||
args: {
|
||||
clusters: ['orange'],
|
||||
activeCluster: false,
|
||||
},
|
||||
};
|
||||
|
||||
export function OneClusterWithActiveCluster() {
|
||||
const cluster = makeRootCluster({
|
||||
loggedInUser: makeLoggedInUser({
|
||||
name: 'alice',
|
||||
roles: ['access', 'editor'],
|
||||
}),
|
||||
});
|
||||
export const OneClusterWithActiveCluster: StoryObj<StoryProps> = {
|
||||
args: {
|
||||
clusters: ['violet'],
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<OpenIdentityPopover clusters={[cluster]} activeClusterUri={cluster.uri} />
|
||||
);
|
||||
}
|
||||
export const ManyClustersWithNoActiveCluster: StoryObj<StoryProps> = {
|
||||
args: {
|
||||
clusters: ['orange', 'green', 'violet'],
|
||||
activeCluster: false,
|
||||
},
|
||||
};
|
||||
|
||||
export function ManyClustersWithNoActiveCluster() {
|
||||
return (
|
||||
<OpenIdentityPopover
|
||||
clusters={[clusterOrange, clusterViolet, clusterGreen]}
|
||||
activeClusterUri={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const ManyClustersWithActiveCluster: StoryObj<StoryProps> = {
|
||||
args: {
|
||||
clusters: ['orange', 'green', 'violet'],
|
||||
},
|
||||
};
|
||||
|
||||
export function ManyClustersWithActiveCluster() {
|
||||
return (
|
||||
<OpenIdentityPopover
|
||||
clusters={[clusterOrange, clusterViolet, clusterGreen]}
|
||||
activeClusterUri={clusterOrange.uri}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const ManyClustersWithProfileErrorsAndActiveCluster: StoryObj<StoryProps> =
|
||||
{
|
||||
args: {
|
||||
clusters: ['orange', 'green', 'violet'],
|
||||
showProfileErrors: true,
|
||||
},
|
||||
};
|
||||
|
||||
export function ManyClustersWithProfileErrorsAndActiveCluster() {
|
||||
return (
|
||||
<OpenIdentityPopover
|
||||
clusters={[
|
||||
makeRootCluster({ ...clusterOrange, profileStatusError }),
|
||||
makeRootCluster({ ...clusterViolet, profileStatusError }),
|
||||
makeRootCluster({ ...clusterGreen, profileStatusError }),
|
||||
]}
|
||||
activeClusterUri={clusterOrange.uri}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const TrustedDeviceEnrolled: StoryObj<StoryProps> = {
|
||||
args: {
|
||||
deviceTrust: 'enrolled',
|
||||
},
|
||||
};
|
||||
|
||||
export function LongNamesWithManyRoles() {
|
||||
return (
|
||||
<OpenIdentityPopover
|
||||
clusters={[
|
||||
clusterOrange,
|
||||
makeRootCluster({
|
||||
...clusterViolet,
|
||||
name: 'psv-eindhoven-eredivisie-production-lorem-ipsum',
|
||||
loggedInUser: makeLoggedInUser({
|
||||
roles: [
|
||||
'circle-mark-app-access',
|
||||
'grafana-lite-app-access',
|
||||
'grafana-gold-app-access',
|
||||
'release-lion-app-access',
|
||||
'release-fox-app-access',
|
||||
'sales-center-lorem-app-access',
|
||||
'sales-center-ipsum-db-access',
|
||||
'sales-center-shop-app-access',
|
||||
'sales-center-floor-db-access',
|
||||
],
|
||||
name: 'ruud-van-nistelrooy-van-der-sar',
|
||||
}),
|
||||
}),
|
||||
clusterGreen,
|
||||
]}
|
||||
activeClusterUri={clusterViolet.uri}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const TrustedDeviceRequiredButNotEnrolled: StoryObj<StoryProps> = {
|
||||
args: {
|
||||
deviceTrust: 'required-not-enrolled',
|
||||
},
|
||||
};
|
||||
|
||||
export function TrustedDeviceEnrolled() {
|
||||
return (
|
||||
<OpenIdentityPopover
|
||||
clusters={[
|
||||
clusterOrange,
|
||||
makeRootCluster({
|
||||
...clusterViolet,
|
||||
loggedInUser: makeLoggedInUser({
|
||||
isDeviceTrusted: true,
|
||||
roles: ['circle-mark-app-access', 'grafana-lite-app-access'],
|
||||
}),
|
||||
}),
|
||||
]}
|
||||
activeClusterUri={clusterViolet.uri}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TrustedDeviceRequiredButNotEnrolled() {
|
||||
return (
|
||||
<OpenIdentityPopover
|
||||
clusters={[
|
||||
clusterOrange,
|
||||
makeRootCluster({
|
||||
...clusterViolet,
|
||||
loggedInUser: makeLoggedInUser({
|
||||
trustedDeviceRequirement: TrustedDeviceRequirement.REQUIRED,
|
||||
roles: ['circle-mark-app-access', 'grafana-lite-app-access'],
|
||||
}),
|
||||
}),
|
||||
]}
|
||||
activeClusterUri={clusterViolet.uri}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const ActiveClusterExpired: StoryObj<StoryProps> = {
|
||||
args: {
|
||||
activeClusterExpired: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,12 +16,20 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { formatDistanceToNowStrict, isPast } from 'date-fns';
|
||||
import { JSX } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ButtonText, Flex, Label, P3 } from 'design';
|
||||
import { Logout, Refresh, ShieldCheck, ShieldWarning } from 'design/Icon';
|
||||
import { ButtonText, Flex, Label, P3, Stack } from 'design';
|
||||
import {
|
||||
Clock,
|
||||
Logout,
|
||||
Refresh,
|
||||
ShieldCheck,
|
||||
ShieldWarning,
|
||||
} from 'design/Icon';
|
||||
import Link from 'design/Link';
|
||||
import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
|
||||
import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
||||
|
||||
import { ProfileStatusError } from 'teleterm/ui/components/ProfileStatusError';
|
||||
@@ -46,6 +54,10 @@ export function ActiveCluster(props: {
|
||||
onLogout(): void;
|
||||
}) {
|
||||
const clusterName = routing.parseClusterName(props.activeCluster.uri);
|
||||
const validUntil =
|
||||
props.activeCluster.loggedInUser?.validUntil &&
|
||||
Timestamp.toDate(props.activeCluster.loggedInUser.validUntil);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex p={3} pb={2} flexWrap="nowrap" gap={2} flexDirection="column">
|
||||
@@ -103,7 +115,35 @@ export function ActiveCluster(props: {
|
||||
</Label>
|
||||
))}
|
||||
</Flex>
|
||||
<DeviceTrustMessage status={props.deviceTrustStatus} />
|
||||
<Stack gap={0}>
|
||||
{validUntil && (
|
||||
<Flex gap={1} color="text.slightlyMuted">
|
||||
<Clock size="small" />
|
||||
<P3>
|
||||
{isPast(validUntil) ? (
|
||||
'Session expired.'
|
||||
) : (
|
||||
<>
|
||||
Session expires{' '}
|
||||
<span
|
||||
title={validUntil.toLocaleString()}
|
||||
css={`
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
`}
|
||||
>
|
||||
{formatDistanceToNowStrict(validUntil, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
.
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</P3>
|
||||
</Flex>
|
||||
)}
|
||||
<DeviceTrustMessage status={props.deviceTrustStatus} />
|
||||
</Stack>
|
||||
</Flex>
|
||||
<Separator />
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user