From 8a8534260a27447e1403e1760a50570959669f93 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Wed, 2 Oct 2024 09:44:01 +0100 Subject: [PATCH] Migrate ReverseTunnel Resource APIs from HTTP to gRPC (#46761) * Start hacking on migrating reversetunnel * Migrate ReverseTunnel HTTP API to gRPC * Wire Cache into Presence GRPC service * Add TestPresenceService_ListReverseTunnels * Add UpsertReverseTunnelV2 store method which returns upserted resource * Make API client return reversetunnel on upsert * Add TestListReverseTunnels * Add TestDeleteReverseTunnel * Add TestUpsertReverseTunnel * Missing err check * Remove presenceService from gRPCServer struct * Add deprecations on GetReverseTunnels --- api/client/client.go | 39 ++ .../go/teleport/presence/v1/service.pb.go | 378 ++++++++++++++++-- .../teleport/presence/v1/service_grpc.pb.go | 120 ++++++ api/proto/teleport/presence/v1/service.proto | 40 ++ lib/auth/apiserver.go | 8 +- lib/auth/auth_test.go | 2 +- lib/auth/auth_with_roles.go | 20 +- lib/auth/authclient/api.go | 9 +- lib/auth/authclient/clt.go | 4 +- lib/auth/authclient/http_client.go | 53 --- lib/auth/authclient/httpfallback.go | 106 +++++ lib/auth/grpcserver.go | 16 +- lib/auth/init.go | 2 +- lib/auth/presence/presencev1/service.go | 107 ++++- lib/auth/presence/presencev1/service_test.go | 308 ++++++++++++++ lib/auth/trustedcluster.go | 12 +- lib/auth/trustedcluster_test.go | 2 +- lib/cache/cache.go | 13 - lib/cache/cache_test.go | 26 -- lib/cache/collections.go | 33 -- lib/cache/resource_reverse_tunnel.go | 87 ++++ lib/cache/resource_reverse_tunnel_test.go | 50 +++ lib/reversetunnel/rc_manager_test.go | 3 +- lib/services/local/presence.go | 97 ++++- lib/services/local/presence_test.go | 83 ++++ lib/services/presence.go | 15 +- lib/services/suite/suite.go | 22 +- tool/tctl/common/resource_command.go | 2 +- 28 files changed, 1425 insertions(+), 232 deletions(-) create mode 100644 lib/cache/resource_reverse_tunnel.go create mode 100644 lib/cache/resource_reverse_tunnel_test.go diff --git a/api/client/client.go b/api/client/client.go index bfe3f5518a2ad..2dd877c643adb 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -5084,6 +5084,45 @@ func (c *Client) GetRemoteCluster(ctx context.Context, name string) (types.Remot return rc, trace.Wrap(err) } +// ListReverseTunnels returns a page of remote clusters. +func (c *Client) ListReverseTunnels(ctx context.Context, pageSize int, nextToken string) ([]types.ReverseTunnel, string, error) { + res, err := c.PresenceServiceClient().ListReverseTunnels(ctx, &presencepb.ListReverseTunnelsRequest{ + PageSize: int32(pageSize), + PageToken: nextToken, + }) + if err != nil { + return nil, "", trace.Wrap(err) + } + rcs := make([]types.ReverseTunnel, 0, len(res.ReverseTunnels)) + for _, rc := range res.ReverseTunnels { + rcs = append(rcs, rc) + } + return rcs, res.NextPageToken, nil +} + +// DeleteReverseTunnel deletes a reverse tunnel resource +func (c *Client) DeleteReverseTunnel(ctx context.Context, name string) error { + _, err := c.PresenceServiceClient().DeleteReverseTunnel(ctx, &presencepb.DeleteReverseTunnelRequest{ + Name: name, + }) + return trace.Wrap(err) +} + +// UpsertReverseTunnel creates or updates reverse tunnel resource +func (c *Client) UpsertReverseTunnel(ctx context.Context, rt types.ReverseTunnel) (types.ReverseTunnel, error) { + rtV3, ok := rt.(*types.ReverseTunnelV2) + if !ok { + return nil, trace.BadParameter("unsupported reverse tunnel type %T", rt) + } + res, err := c.PresenceServiceClient().UpsertReverseTunnel(ctx, &presencepb.UpsertReverseTunnelRequest{ + ReverseTunnel: rtV3, + }) + if err != nil { + return nil, trace.Wrap(err) + } + return res, nil +} + // GetRemoteClusters returns all remote clusters. // Deprecated: use ListRemoteClusters instead. func (c *Client) GetRemoteClusters(ctx context.Context) ([]types.RemoteCluster, error) { diff --git a/api/gen/proto/go/teleport/presence/v1/service.pb.go b/api/gen/proto/go/teleport/presence/v1/service.pb.go index f03dbb5cae1fd..1bd5cdc78da0e 100644 --- a/api/gen/proto/go/teleport/presence/v1/service.pb.go +++ b/api/gen/proto/go/teleport/presence/v1/service.pb.go @@ -313,6 +313,223 @@ func (x *DeleteRemoteClusterRequest) GetName() string { return "" } +// Request for ListReverseTunnels +type ListReverseTunnelsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The maximum number of items to return. + // The server may impose a different page size at its discretion. + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // The page_token is the next_page_token value returned from a previous List + // request, if any. + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` +} + +func (x *ListReverseTunnelsRequest) Reset() { + *x = ListReverseTunnelsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_teleport_presence_v1_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListReverseTunnelsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListReverseTunnelsRequest) ProtoMessage() {} + +func (x *ListReverseTunnelsRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_presence_v1_service_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListReverseTunnelsRequest.ProtoReflect.Descriptor instead. +func (*ListReverseTunnelsRequest) Descriptor() ([]byte, []int) { + return file_teleport_presence_v1_service_proto_rawDescGZIP(), []int{5} +} + +func (x *ListReverseTunnelsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListReverseTunnelsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// Response for ListReverseTunnels +type ListReverseTunnelsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // ReverseTunnels is the list of ReverseTunnels that were retrieved. + ReverseTunnels []*types.ReverseTunnelV2 `protobuf:"bytes,1,rep,name=reverse_tunnels,json=reverseTunnels,proto3" json:"reverse_tunnels,omitempty"` + // Token to retrieve the next page of results, or empty if there are no + // more results in the list. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` +} + +func (x *ListReverseTunnelsResponse) Reset() { + *x = ListReverseTunnelsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_teleport_presence_v1_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListReverseTunnelsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListReverseTunnelsResponse) ProtoMessage() {} + +func (x *ListReverseTunnelsResponse) ProtoReflect() protoreflect.Message { + mi := &file_teleport_presence_v1_service_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListReverseTunnelsResponse.ProtoReflect.Descriptor instead. +func (*ListReverseTunnelsResponse) Descriptor() ([]byte, []int) { + return file_teleport_presence_v1_service_proto_rawDescGZIP(), []int{6} +} + +func (x *ListReverseTunnelsResponse) GetReverseTunnels() []*types.ReverseTunnelV2 { + if x != nil { + return x.ReverseTunnels + } + return nil +} + +func (x *ListReverseTunnelsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// Request for UpsertReverseTunnel +type UpsertReverseTunnelRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // ReverseTunnel is the ReverseTunnel to upsert. + ReverseTunnel *types.ReverseTunnelV2 `protobuf:"bytes,1,opt,name=reverse_tunnel,json=reverseTunnel,proto3" json:"reverse_tunnel,omitempty"` +} + +func (x *UpsertReverseTunnelRequest) Reset() { + *x = UpsertReverseTunnelRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_teleport_presence_v1_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpsertReverseTunnelRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpsertReverseTunnelRequest) ProtoMessage() {} + +func (x *UpsertReverseTunnelRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_presence_v1_service_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpsertReverseTunnelRequest.ProtoReflect.Descriptor instead. +func (*UpsertReverseTunnelRequest) Descriptor() ([]byte, []int) { + return file_teleport_presence_v1_service_proto_rawDescGZIP(), []int{7} +} + +func (x *UpsertReverseTunnelRequest) GetReverseTunnel() *types.ReverseTunnelV2 { + if x != nil { + return x.ReverseTunnel + } + return nil +} + +// Request for DeleteReverseTunnel +type DeleteReverseTunnelRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Name is the name of the ReverseTunnel to delete. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *DeleteReverseTunnelRequest) Reset() { + *x = DeleteReverseTunnelRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_teleport_presence_v1_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteReverseTunnelRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteReverseTunnelRequest) ProtoMessage() {} + +func (x *DeleteReverseTunnelRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_presence_v1_service_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteReverseTunnelRequest.ProtoReflect.Descriptor instead. +func (*DeleteReverseTunnelRequest) Descriptor() ([]byte, []int) { + return file_teleport_presence_v1_service_proto_rawDescGZIP(), []int{8} +} + +func (x *DeleteReverseTunnelRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + var File_teleport_presence_v1_service_proto protoreflect.FileDescriptor var file_teleport_presence_v1_service_proto_rawDesc = []byte{ @@ -355,7 +572,30 @@ var file_teleport_presence_v1_service_proto_rawDesc = []byte{ 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x22, 0x30, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x32, 0xa7, 0x03, 0x0a, 0x0f, 0x50, 0x72, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x57, 0x0a, 0x19, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, + 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, + 0x69, 0x7a, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x22, 0x85, 0x01, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x76, 0x65, 0x72, + 0x73, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x3f, 0x0a, 0x0f, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f, 0x74, 0x75, 0x6e, + 0x6e, 0x65, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x79, 0x70, + 0x65, 0x73, 0x2e, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, + 0x56, 0x32, 0x52, 0x0e, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, + 0x6c, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, + 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x5b, 0x0a, 0x1a, 0x55, 0x70, + 0x73, 0x65, 0x72, 0x74, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, + 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3d, 0x0a, 0x0e, 0x72, 0x65, 0x76, 0x65, + 0x72, 0x73, 0x65, 0x5f, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, + 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x56, 0x32, 0x52, 0x0d, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, + 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x22, 0x30, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x32, 0xe2, 0x05, 0x0a, 0x0f, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x59, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x70, 0x72, 0x65, @@ -382,13 +622,32 @@ var file_teleport_presence_v1_service_proto_rawDesc = []byte{ 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 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, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, 0x6e, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2f, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, - 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x70, 0x74, 0x79, 0x12, 0x77, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x76, 0x65, 0x72, + 0x73, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x12, 0x2f, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x54, 0x75, 0x6e, 0x6e, + 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x54, 0x75, 0x6e, + 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5f, 0x0a, 0x13, + 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x54, 0x75, 0x6e, + 0x6e, 0x65, 0x6c, 0x12, 0x30, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x70, + 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, + 0x74, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x52, 0x65, + 0x76, 0x65, 0x72, 0x73, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x56, 0x32, 0x12, 0x5f, 0x0a, + 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x54, 0x75, + 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x30, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 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, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x70, 0x72, + 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, + 0x63, 0x65, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -403,34 +662,47 @@ func file_teleport_presence_v1_service_proto_rawDescGZIP() []byte { return file_teleport_presence_v1_service_proto_rawDescData } -var file_teleport_presence_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_teleport_presence_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_teleport_presence_v1_service_proto_goTypes = []any{ (*GetRemoteClusterRequest)(nil), // 0: teleport.presence.v1.GetRemoteClusterRequest (*ListRemoteClustersRequest)(nil), // 1: teleport.presence.v1.ListRemoteClustersRequest (*ListRemoteClustersResponse)(nil), // 2: teleport.presence.v1.ListRemoteClustersResponse (*UpdateRemoteClusterRequest)(nil), // 3: teleport.presence.v1.UpdateRemoteClusterRequest (*DeleteRemoteClusterRequest)(nil), // 4: teleport.presence.v1.DeleteRemoteClusterRequest - (*types.RemoteClusterV3)(nil), // 5: types.RemoteClusterV3 - (*fieldmaskpb.FieldMask)(nil), // 6: google.protobuf.FieldMask - (*emptypb.Empty)(nil), // 7: google.protobuf.Empty + (*ListReverseTunnelsRequest)(nil), // 5: teleport.presence.v1.ListReverseTunnelsRequest + (*ListReverseTunnelsResponse)(nil), // 6: teleport.presence.v1.ListReverseTunnelsResponse + (*UpsertReverseTunnelRequest)(nil), // 7: teleport.presence.v1.UpsertReverseTunnelRequest + (*DeleteReverseTunnelRequest)(nil), // 8: teleport.presence.v1.DeleteReverseTunnelRequest + (*types.RemoteClusterV3)(nil), // 9: types.RemoteClusterV3 + (*fieldmaskpb.FieldMask)(nil), // 10: google.protobuf.FieldMask + (*types.ReverseTunnelV2)(nil), // 11: types.ReverseTunnelV2 + (*emptypb.Empty)(nil), // 12: google.protobuf.Empty } var file_teleport_presence_v1_service_proto_depIdxs = []int32{ - 5, // 0: teleport.presence.v1.ListRemoteClustersResponse.remote_clusters:type_name -> types.RemoteClusterV3 - 5, // 1: teleport.presence.v1.UpdateRemoteClusterRequest.remote_cluster:type_name -> types.RemoteClusterV3 - 6, // 2: teleport.presence.v1.UpdateRemoteClusterRequest.update_mask:type_name -> google.protobuf.FieldMask - 0, // 3: teleport.presence.v1.PresenceService.GetRemoteCluster:input_type -> teleport.presence.v1.GetRemoteClusterRequest - 1, // 4: teleport.presence.v1.PresenceService.ListRemoteClusters:input_type -> teleport.presence.v1.ListRemoteClustersRequest - 3, // 5: teleport.presence.v1.PresenceService.UpdateRemoteCluster:input_type -> teleport.presence.v1.UpdateRemoteClusterRequest - 4, // 6: teleport.presence.v1.PresenceService.DeleteRemoteCluster:input_type -> teleport.presence.v1.DeleteRemoteClusterRequest - 5, // 7: teleport.presence.v1.PresenceService.GetRemoteCluster:output_type -> types.RemoteClusterV3 - 2, // 8: teleport.presence.v1.PresenceService.ListRemoteClusters:output_type -> teleport.presence.v1.ListRemoteClustersResponse - 5, // 9: teleport.presence.v1.PresenceService.UpdateRemoteCluster:output_type -> types.RemoteClusterV3 - 7, // 10: teleport.presence.v1.PresenceService.DeleteRemoteCluster:output_type -> google.protobuf.Empty - 7, // [7:11] is the sub-list for method output_type - 3, // [3:7] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 9, // 0: teleport.presence.v1.ListRemoteClustersResponse.remote_clusters:type_name -> types.RemoteClusterV3 + 9, // 1: teleport.presence.v1.UpdateRemoteClusterRequest.remote_cluster:type_name -> types.RemoteClusterV3 + 10, // 2: teleport.presence.v1.UpdateRemoteClusterRequest.update_mask:type_name -> google.protobuf.FieldMask + 11, // 3: teleport.presence.v1.ListReverseTunnelsResponse.reverse_tunnels:type_name -> types.ReverseTunnelV2 + 11, // 4: teleport.presence.v1.UpsertReverseTunnelRequest.reverse_tunnel:type_name -> types.ReverseTunnelV2 + 0, // 5: teleport.presence.v1.PresenceService.GetRemoteCluster:input_type -> teleport.presence.v1.GetRemoteClusterRequest + 1, // 6: teleport.presence.v1.PresenceService.ListRemoteClusters:input_type -> teleport.presence.v1.ListRemoteClustersRequest + 3, // 7: teleport.presence.v1.PresenceService.UpdateRemoteCluster:input_type -> teleport.presence.v1.UpdateRemoteClusterRequest + 4, // 8: teleport.presence.v1.PresenceService.DeleteRemoteCluster:input_type -> teleport.presence.v1.DeleteRemoteClusterRequest + 5, // 9: teleport.presence.v1.PresenceService.ListReverseTunnels:input_type -> teleport.presence.v1.ListReverseTunnelsRequest + 7, // 10: teleport.presence.v1.PresenceService.UpsertReverseTunnel:input_type -> teleport.presence.v1.UpsertReverseTunnelRequest + 8, // 11: teleport.presence.v1.PresenceService.DeleteReverseTunnel:input_type -> teleport.presence.v1.DeleteReverseTunnelRequest + 9, // 12: teleport.presence.v1.PresenceService.GetRemoteCluster:output_type -> types.RemoteClusterV3 + 2, // 13: teleport.presence.v1.PresenceService.ListRemoteClusters:output_type -> teleport.presence.v1.ListRemoteClustersResponse + 9, // 14: teleport.presence.v1.PresenceService.UpdateRemoteCluster:output_type -> types.RemoteClusterV3 + 12, // 15: teleport.presence.v1.PresenceService.DeleteRemoteCluster:output_type -> google.protobuf.Empty + 6, // 16: teleport.presence.v1.PresenceService.ListReverseTunnels:output_type -> teleport.presence.v1.ListReverseTunnelsResponse + 11, // 17: teleport.presence.v1.PresenceService.UpsertReverseTunnel:output_type -> types.ReverseTunnelV2 + 12, // 18: teleport.presence.v1.PresenceService.DeleteReverseTunnel:output_type -> google.protobuf.Empty + 12, // [12:19] is the sub-list for method output_type + 5, // [5:12] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_teleport_presence_v1_service_proto_init() } @@ -499,6 +771,54 @@ func file_teleport_presence_v1_service_proto_init() { return nil } } + file_teleport_presence_v1_service_proto_msgTypes[5].Exporter = func(v any, i int) any { + switch v := v.(*ListReverseTunnelsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_teleport_presence_v1_service_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*ListReverseTunnelsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_teleport_presence_v1_service_proto_msgTypes[7].Exporter = func(v any, i int) any { + switch v := v.(*UpsertReverseTunnelRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_teleport_presence_v1_service_proto_msgTypes[8].Exporter = func(v any, i int) any { + switch v := v.(*DeleteReverseTunnelRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -506,7 +826,7 @@ func file_teleport_presence_v1_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_teleport_presence_v1_service_proto_rawDesc, NumEnums: 0, - NumMessages: 5, + NumMessages: 9, NumExtensions: 0, NumServices: 1, }, diff --git a/api/gen/proto/go/teleport/presence/v1/service_grpc.pb.go b/api/gen/proto/go/teleport/presence/v1/service_grpc.pb.go index 84d3a1acabc58..ac6ead13f60db 100644 --- a/api/gen/proto/go/teleport/presence/v1/service_grpc.pb.go +++ b/api/gen/proto/go/teleport/presence/v1/service_grpc.pb.go @@ -39,6 +39,9 @@ const ( PresenceService_ListRemoteClusters_FullMethodName = "/teleport.presence.v1.PresenceService/ListRemoteClusters" PresenceService_UpdateRemoteCluster_FullMethodName = "/teleport.presence.v1.PresenceService/UpdateRemoteCluster" PresenceService_DeleteRemoteCluster_FullMethodName = "/teleport.presence.v1.PresenceService/DeleteRemoteCluster" + PresenceService_ListReverseTunnels_FullMethodName = "/teleport.presence.v1.PresenceService/ListReverseTunnels" + PresenceService_UpsertReverseTunnel_FullMethodName = "/teleport.presence.v1.PresenceService/UpsertReverseTunnel" + PresenceService_DeleteReverseTunnel_FullMethodName = "/teleport.presence.v1.PresenceService/DeleteReverseTunnel" ) // PresenceServiceClient is the client API for PresenceService service. @@ -55,6 +58,12 @@ type PresenceServiceClient interface { UpdateRemoteCluster(ctx context.Context, in *UpdateRemoteClusterRequest, opts ...grpc.CallOption) (*types.RemoteClusterV3, error) // DeleteRemoteCluster removes an existing RemoteCluster by name. DeleteRemoteCluster(ctx context.Context, in *DeleteRemoteClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // ListReverseTunnels retrieves a page of ReverseTunnels. + ListReverseTunnels(ctx context.Context, in *ListReverseTunnelsRequest, opts ...grpc.CallOption) (*ListReverseTunnelsResponse, error) + // UpsertReverseTunnel upserts a ReverseTunnel. + UpsertReverseTunnel(ctx context.Context, in *UpsertReverseTunnelRequest, opts ...grpc.CallOption) (*types.ReverseTunnelV2, error) + // DeleteReverseTunnel removes an existing ReverseTunnel by name. + DeleteReverseTunnel(ctx context.Context, in *DeleteReverseTunnelRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) } type presenceServiceClient struct { @@ -105,6 +114,36 @@ func (c *presenceServiceClient) DeleteRemoteCluster(ctx context.Context, in *Del return out, nil } +func (c *presenceServiceClient) ListReverseTunnels(ctx context.Context, in *ListReverseTunnelsRequest, opts ...grpc.CallOption) (*ListReverseTunnelsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListReverseTunnelsResponse) + err := c.cc.Invoke(ctx, PresenceService_ListReverseTunnels_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *presenceServiceClient) UpsertReverseTunnel(ctx context.Context, in *UpsertReverseTunnelRequest, opts ...grpc.CallOption) (*types.ReverseTunnelV2, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(types.ReverseTunnelV2) + err := c.cc.Invoke(ctx, PresenceService_UpsertReverseTunnel_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *presenceServiceClient) DeleteReverseTunnel(ctx context.Context, in *DeleteReverseTunnelRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, PresenceService_DeleteReverseTunnel_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // PresenceServiceServer is the server API for PresenceService service. // All implementations must embed UnimplementedPresenceServiceServer // for forward compatibility. @@ -119,6 +158,12 @@ type PresenceServiceServer interface { UpdateRemoteCluster(context.Context, *UpdateRemoteClusterRequest) (*types.RemoteClusterV3, error) // DeleteRemoteCluster removes an existing RemoteCluster by name. DeleteRemoteCluster(context.Context, *DeleteRemoteClusterRequest) (*emptypb.Empty, error) + // ListReverseTunnels retrieves a page of ReverseTunnels. + ListReverseTunnels(context.Context, *ListReverseTunnelsRequest) (*ListReverseTunnelsResponse, error) + // UpsertReverseTunnel upserts a ReverseTunnel. + UpsertReverseTunnel(context.Context, *UpsertReverseTunnelRequest) (*types.ReverseTunnelV2, error) + // DeleteReverseTunnel removes an existing ReverseTunnel by name. + DeleteReverseTunnel(context.Context, *DeleteReverseTunnelRequest) (*emptypb.Empty, error) mustEmbedUnimplementedPresenceServiceServer() } @@ -141,6 +186,15 @@ func (UnimplementedPresenceServiceServer) UpdateRemoteCluster(context.Context, * func (UnimplementedPresenceServiceServer) DeleteRemoteCluster(context.Context, *DeleteRemoteClusterRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteRemoteCluster not implemented") } +func (UnimplementedPresenceServiceServer) ListReverseTunnels(context.Context, *ListReverseTunnelsRequest) (*ListReverseTunnelsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListReverseTunnels not implemented") +} +func (UnimplementedPresenceServiceServer) UpsertReverseTunnel(context.Context, *UpsertReverseTunnelRequest) (*types.ReverseTunnelV2, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpsertReverseTunnel not implemented") +} +func (UnimplementedPresenceServiceServer) DeleteReverseTunnel(context.Context, *DeleteReverseTunnelRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteReverseTunnel not implemented") +} func (UnimplementedPresenceServiceServer) mustEmbedUnimplementedPresenceServiceServer() {} func (UnimplementedPresenceServiceServer) testEmbeddedByValue() {} @@ -234,6 +288,60 @@ func _PresenceService_DeleteRemoteCluster_Handler(srv interface{}, ctx context.C return interceptor(ctx, in, info, handler) } +func _PresenceService_ListReverseTunnels_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListReverseTunnelsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PresenceServiceServer).ListReverseTunnels(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PresenceService_ListReverseTunnels_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PresenceServiceServer).ListReverseTunnels(ctx, req.(*ListReverseTunnelsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PresenceService_UpsertReverseTunnel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpsertReverseTunnelRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PresenceServiceServer).UpsertReverseTunnel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PresenceService_UpsertReverseTunnel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PresenceServiceServer).UpsertReverseTunnel(ctx, req.(*UpsertReverseTunnelRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PresenceService_DeleteReverseTunnel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteReverseTunnelRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PresenceServiceServer).DeleteReverseTunnel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PresenceService_DeleteReverseTunnel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PresenceServiceServer).DeleteReverseTunnel(ctx, req.(*DeleteReverseTunnelRequest)) + } + return interceptor(ctx, in, info, handler) +} + // PresenceService_ServiceDesc is the grpc.ServiceDesc for PresenceService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -257,6 +365,18 @@ var PresenceService_ServiceDesc = grpc.ServiceDesc{ MethodName: "DeleteRemoteCluster", Handler: _PresenceService_DeleteRemoteCluster_Handler, }, + { + MethodName: "ListReverseTunnels", + Handler: _PresenceService_ListReverseTunnels_Handler, + }, + { + MethodName: "UpsertReverseTunnel", + Handler: _PresenceService_UpsertReverseTunnel_Handler, + }, + { + MethodName: "DeleteReverseTunnel", + Handler: _PresenceService_DeleteReverseTunnel_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "teleport/presence/v1/service.proto", diff --git a/api/proto/teleport/presence/v1/service.proto b/api/proto/teleport/presence/v1/service.proto index 4816be7c87ac3..325d5b68ee093 100644 --- a/api/proto/teleport/presence/v1/service.proto +++ b/api/proto/teleport/presence/v1/service.proto @@ -32,6 +32,13 @@ service PresenceService { rpc UpdateRemoteCluster(UpdateRemoteClusterRequest) returns (types.RemoteClusterV3); // DeleteRemoteCluster removes an existing RemoteCluster by name. rpc DeleteRemoteCluster(DeleteRemoteClusterRequest) returns (google.protobuf.Empty); + + // ListReverseTunnels retrieves a page of ReverseTunnels. + rpc ListReverseTunnels(ListReverseTunnelsRequest) returns (ListReverseTunnelsResponse); + // UpsertReverseTunnel upserts a ReverseTunnel. + rpc UpsertReverseTunnel(UpsertReverseTunnelRequest) returns (types.ReverseTunnelV2); + // DeleteReverseTunnel removes an existing ReverseTunnel by name. + rpc DeleteReverseTunnel(DeleteReverseTunnelRequest) returns (google.protobuf.Empty); } // Request for GetRemoteCluster @@ -75,3 +82,36 @@ message DeleteRemoteClusterRequest { // Name is the name of the RemoteCluster to delete. string name = 1; } + +// Request for ListReverseTunnels +message ListReverseTunnelsRequest { + // The maximum number of items to return. + // The server may impose a different page size at its discretion. + int32 page_size = 1; + + // The page_token is the next_page_token value returned from a previous List + // request, if any. + string page_token = 2; +} + +// Response for ListReverseTunnels +message ListReverseTunnelsResponse { + // ReverseTunnels is the list of ReverseTunnels that were retrieved. + repeated types.ReverseTunnelV2 reverse_tunnels = 1; + + // Token to retrieve the next page of results, or empty if there are no + // more results in the list. + string next_page_token = 2; +} + +// Request for UpsertReverseTunnel +message UpsertReverseTunnelRequest { + // ReverseTunnel is the ReverseTunnel to upsert. + types.ReverseTunnelV2 reverse_tunnel = 1; +} + +// Request for DeleteReverseTunnel +message DeleteReverseTunnelRequest { + // Name is the name of the ReverseTunnel to delete. + string name = 1; +} diff --git a/lib/auth/apiserver.go b/lib/auth/apiserver.go index 92c87d029bba2..3d3bea02919ca 100644 --- a/lib/auth/apiserver.go +++ b/lib/auth/apiserver.go @@ -136,6 +136,7 @@ func NewAPIServer(config *APIConfig) (http.Handler, error) { srv.DELETE("/:version/tunnelconnections", srv.WithAuth(srv.deleteAllTunnelConnections)) // Reverse tunnels + // TODO(noah): DELETE IN 18.0.0 - all these methods are now gRPC. srv.POST("/:version/reversetunnels", srv.WithAuth(srv.upsertReverseTunnel)) srv.GET("/:version/reversetunnels", srv.WithAuth(srv.getReverseTunnels)) srv.DELETE("/:version/reversetunnels/:domain", srv.WithAuth(srv.deleteReverseTunnel)) @@ -344,6 +345,7 @@ type upsertReverseTunnelRawReq struct { } // upsertReverseTunnel is called by admin to create a reverse tunnel to remote proxy +// TODO(noah): DELETE IN 18.0.0 - all these methods are now gRPC. func (s *APIServer) upsertReverseTunnel(auth *ServerWithRoles, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { var req upsertReverseTunnelRawReq if err := httplib.ReadJSON(r, &req); err != nil { @@ -359,13 +361,14 @@ func (s *APIServer) upsertReverseTunnel(auth *ServerWithRoles, w http.ResponseWr if req.TTL != 0 { tun.SetExpiry(s.Now().UTC().Add(req.TTL)) } - if err := auth.UpsertReverseTunnel(tun); err != nil { + if err := auth.UpsertReverseTunnel(r.Context(), tun); err != nil { return nil, trace.Wrap(err) } return message("ok"), nil } // getReverseTunnels returns a list of reverse tunnels +// TODO(noah): DELETE IN 18.0.0 - all these methods are now gRPC. func (s *APIServer) getReverseTunnels(auth *ServerWithRoles, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { reverseTunnels, err := auth.GetReverseTunnels(r.Context()) if err != nil { @@ -383,9 +386,10 @@ func (s *APIServer) getReverseTunnels(auth *ServerWithRoles, w http.ResponseWrit } // deleteReverseTunnel deletes reverse tunnel +// TODO(noah): DELETE IN 18.0.0 - all these methods are now gRPC. func (s *APIServer) deleteReverseTunnel(auth *ServerWithRoles, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { domainName := p.ByName("domain") - err := auth.DeleteReverseTunnel(domainName) + err := auth.DeleteReverseTunnel(r.Context(), domainName) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/auth_test.go b/lib/auth/auth_test.go index 68d4eaf93b958..99545b4a72d25 100644 --- a/lib/auth/auth_test.go +++ b/lib/auth/auth_test.go @@ -1305,7 +1305,7 @@ func TestTrustedClusterCRUDEventEmitted(t *testing.T) { require.NoError(t, s.a.UpsertCertAuthority(ctx, suite.NewTestCA(types.UserCA, "test"))) require.NoError(t, s.a.UpsertCertAuthority(ctx, suite.NewTestCA(types.HostCA, "test"))) - err = s.a.createReverseTunnel(tc) + err = s.a.createReverseTunnel(ctx, tc) require.NoError(t, err) // test create event for switch case: when tc exists but enabled is false diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 696a303eccdf4..141cdf3ff01d8 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -2131,32 +2131,28 @@ func (a *ServerWithRoles) DeleteProxy(ctx context.Context, name string) error { return a.authServer.DeleteProxy(ctx, name) } -func (a *ServerWithRoles) UpsertReverseTunnel(r types.ReverseTunnel) error { +// TODO(noah): DELETE IN 18.0.0 - all these methods are now gRPC. +func (a *ServerWithRoles) UpsertReverseTunnel(ctx context.Context, r types.ReverseTunnel) error { if err := a.action(apidefaults.Namespace, types.KindReverseTunnel, types.VerbCreate, types.VerbUpdate); err != nil { return trace.Wrap(err) } - return a.authServer.UpsertReverseTunnel(r) -} - -func (a *ServerWithRoles) GetReverseTunnel(name string, opts ...services.MarshalOption) (types.ReverseTunnel, error) { - if err := a.action(apidefaults.Namespace, types.KindReverseTunnel, types.VerbRead); err != nil { - return nil, trace.Wrap(err) - } - return a.authServer.GetReverseTunnel(name, opts...) + return a.authServer.UpsertReverseTunnel(ctx, r) } +// TODO(noah): DELETE IN 18.0.0 - all these methods are now gRPC. func (a *ServerWithRoles) GetReverseTunnels(ctx context.Context, opts ...services.MarshalOption) ([]types.ReverseTunnel, error) { if err := a.action(apidefaults.Namespace, types.KindReverseTunnel, types.VerbList, types.VerbRead); err != nil { return nil, trace.Wrap(err) } - return a.authServer.GetReverseTunnels(ctx, opts...) + return a.authServer.GetReverseTunnels(ctx) } -func (a *ServerWithRoles) DeleteReverseTunnel(domainName string) error { +// TODO(noah): DELETE IN 18.0.0 - all these methods are now gRPC. +func (a *ServerWithRoles) DeleteReverseTunnel(ctx context.Context, domainName string) error { if err := a.action(apidefaults.Namespace, types.KindReverseTunnel, types.VerbDelete); err != nil { return trace.Wrap(err) } - return a.authServer.DeleteReverseTunnel(domainName) + return a.authServer.DeleteReverseTunnel(ctx, domainName) } func (a *ServerWithRoles) DeleteToken(ctx context.Context, token string) error { diff --git a/lib/auth/authclient/api.go b/lib/auth/authclient/api.go index d3f8080c44181..f3fadb2e1959a 100644 --- a/lib/auth/authclient/api.go +++ b/lib/auth/authclient/api.go @@ -224,7 +224,7 @@ type ReadProxyAccessPoint interface { GetAuthServers() ([]types.Server, error) // GetReverseTunnels returns a list of reverse tunnels - GetReverseTunnels(ctx context.Context, opts ...services.MarshalOption) ([]types.ReverseTunnel, error) + GetReverseTunnels(ctx context.Context) ([]types.ReverseTunnel, error) // GetAllTunnelConnections returns all tunnel connections GetAllTunnelConnections(opts ...services.MarshalOption) ([]types.TunnelConnection, error) @@ -391,7 +391,7 @@ type ReadRemoteProxyAccessPoint interface { GetAuthServers() ([]types.Server, error) // GetReverseTunnels returns a list of reverse tunnels - GetReverseTunnels(ctx context.Context, opts ...services.MarshalOption) ([]types.ReverseTunnel, error) + GetReverseTunnels(ctx context.Context) ([]types.ReverseTunnel, error) // GetAllTunnelConnections returns all tunnel connections GetAllTunnelConnections(opts ...services.MarshalOption) ([]types.TunnelConnection, error) @@ -953,7 +953,10 @@ type Cache interface { NewWatcher(ctx context.Context, watch types.Watch) (types.Watcher, error) // GetReverseTunnels returns a list of reverse tunnels - GetReverseTunnels(ctx context.Context, opts ...services.MarshalOption) ([]types.ReverseTunnel, error) + GetReverseTunnels(ctx context.Context) ([]types.ReverseTunnel, error) + + // ListReverseTunnels returns a paginated list of reverse tunnels. + ListReverseTunnels(ctx context.Context, pageSize int, pageToken string) ([]types.ReverseTunnel, string, error) // GetClusterName returns cluster name GetClusterName(opts ...services.MarshalOption) (types.ClusterName, error) diff --git a/lib/auth/authclient/clt.go b/lib/auth/authclient/clt.go index 97134c4ef3169..e11d303579db8 100644 --- a/lib/auth/authclient/clt.go +++ b/lib/auth/authclient/clt.go @@ -295,7 +295,7 @@ func (c *Client) KeepAliveServer(ctx context.Context, keepAlive types.KeepAlive) } // GetReverseTunnel not implemented: can only be called locally. -func (c *Client) GetReverseTunnel(name string, opts ...services.MarshalOption) (types.ReverseTunnel, error) { +func (c *Client) GetReverseTunnel(ctx context.Context, name string) (types.ReverseTunnel, error) { return nil, trace.NotImplemented(notImplementedMessage) } @@ -377,7 +377,7 @@ func (c *Client) DeleteAllCertAuthorities(caType types.CertAuthType) error { } // DeleteAllReverseTunnels not implemented: can only be called locally. -func (c *Client) DeleteAllReverseTunnels() error { +func (c *Client) DeleteAllReverseTunnels(ctx context.Context) error { return trace.NotImplemented(notImplementedMessage) } diff --git a/lib/auth/authclient/http_client.go b/lib/auth/authclient/http_client.go index a6ee3dbff73bb..5cf5119ad90f9 100644 --- a/lib/auth/authclient/http_client.go +++ b/lib/auth/authclient/http_client.go @@ -24,7 +24,6 @@ import ( "encoding/json" "net/http" "net/url" - "strings" "time" "github.com/gravitational/roundtrip" @@ -356,58 +355,6 @@ func (c *HTTPClient) RegisterUsingToken(ctx context.Context, req *types.Register return &certs, nil } -type upsertReverseTunnelRawReq struct { - ReverseTunnel json.RawMessage `json:"reverse_tunnel"` - TTL time.Duration `json:"ttl"` -} - -// UpsertReverseTunnel is used by admins to create a new reverse tunnel -// to the remote proxy to bypass firewall restrictions -func (c *HTTPClient) UpsertReverseTunnel(tunnel types.ReverseTunnel) error { - data, err := services.MarshalReverseTunnel(tunnel) - if err != nil { - return trace.Wrap(err) - } - args := &upsertReverseTunnelRawReq{ - ReverseTunnel: data, - } - _, err = c.PostJSON(context.TODO(), c.Endpoint("reversetunnels"), args) - return trace.Wrap(err) -} - -// GetReverseTunnels returns the list of created reverse tunnels -func (c *HTTPClient) GetReverseTunnels(ctx context.Context, opts ...services.MarshalOption) ([]types.ReverseTunnel, error) { - out, err := c.Get(ctx, c.Endpoint("reversetunnels"), url.Values{}) - if err != nil { - return nil, trace.Wrap(err) - } - var items []json.RawMessage - if err := json.Unmarshal(out.Bytes(), &items); err != nil { - return nil, trace.Wrap(err) - } - tunnels := make([]types.ReverseTunnel, len(items)) - for i, raw := range items { - tunnel, err := services.UnmarshalReverseTunnel(raw) - if err != nil { - return nil, trace.Wrap(err) - } - tunnels[i] = tunnel - } - return tunnels, nil -} - -// DeleteReverseTunnel deletes reverse tunnel by domain name -func (c *HTTPClient) DeleteReverseTunnel(domainName string) error { - // this is to avoid confusing error in case if domain empty for example - // HTTP route will fail producing generic not found error - // instead we catch the error here - if strings.TrimSpace(domainName) == "" { - return trace.BadParameter("empty domain name") - } - _, err := c.Delete(context.TODO(), c.Endpoint("reversetunnels", domainName)) - return trace.Wrap(err) -} - type upsertTunnelConnectionRawReq struct { TunnelConnection json.RawMessage `json:"tunnel_connection"` } diff --git a/lib/auth/authclient/httpfallback.go b/lib/auth/authclient/httpfallback.go index 7287e1ec923f6..58d665e12ba11 100644 --- a/lib/auth/authclient/httpfallback.go +++ b/lib/auth/authclient/httpfallback.go @@ -18,5 +18,111 @@ package authclient +import ( + "context" + "encoding/json" + "net/url" + "strings" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/services" +) + // httpfallback.go holds endpoints that have been converted to gRPC // but still need http fallback logic in the old client. + +// GetReverseTunnels returns the list of created reverse tunnels +// TODO(noah): DELETE IN 18.0.0 +func (c *Client) GetReverseTunnels(ctx context.Context) ([]types.ReverseTunnel, error) { + var rcs []types.ReverseTunnel + pageToken := "" + for { + page, nextToken, err := c.APIClient.ListReverseTunnels(ctx, 0, pageToken) + if err != nil { + if trace.IsNotImplemented(err) { + return c.getReverseTunnelsLegacy(ctx) + } + return nil, trace.Wrap(err) + } + rcs = append(rcs, page...) + if nextToken == "" { + return rcs, nil + } + pageToken = nextToken + } +} + +func (c *Client) getReverseTunnelsLegacy(ctx context.Context) ([]types.ReverseTunnel, error) { + out, err := c.Get(ctx, c.Endpoint("reversetunnels"), url.Values{}) + if err != nil { + return nil, trace.Wrap(err) + } + var items []json.RawMessage + if err := json.Unmarshal(out.Bytes(), &items); err != nil { + return nil, trace.Wrap(err) + } + tunnels := make([]types.ReverseTunnel, len(items)) + for i, raw := range items { + tunnel, err := services.UnmarshalReverseTunnel(raw) + if err != nil { + return nil, trace.Wrap(err) + } + tunnels[i] = tunnel + } + return tunnels, nil +} + +// UpsertReverseTunnel upserts a reverse tunnel +// TODO: DELETE IN 18.0.0 +func (c *Client) UpsertReverseTunnel(ctx context.Context, tunnel types.ReverseTunnel) error { + _, err := c.APIClient.UpsertReverseTunnel(ctx, tunnel) + if err == nil { + return nil + } + if !trace.IsNotImplemented(err) { + return trace.Wrap(err) + } + return c.upsertReverseTunnelLegacy(context.Background(), tunnel) +} + +type upsertReverseTunnelRawReq struct { + ReverseTunnel json.RawMessage `json:"reverse_tunnel"` +} + +func (c *Client) upsertReverseTunnelLegacy(ctx context.Context, tunnel types.ReverseTunnel) error { + data, err := services.MarshalReverseTunnel(tunnel) + if err != nil { + return trace.Wrap(err) + } + args := &upsertReverseTunnelRawReq{ + ReverseTunnel: data, + } + _, err = c.PostJSON(ctx, c.Endpoint("reversetunnels"), args) + return trace.Wrap(err) +} + +// DeleteReverseTunnel deletes reverse tunnel by name +// TODO(noah): DELETE IN 18.0.0 +func (c *Client) DeleteReverseTunnel(ctx context.Context, name string) error { + err := c.APIClient.DeleteReverseTunnel(ctx, name) + if err == nil { + return nil + } + if !trace.IsNotImplemented(err) { + return trace.Wrap(err) + } + return c.deleteReverseTunnelLegacy(ctx, name) +} + +func (c *Client) deleteReverseTunnelLegacy(ctx context.Context, domainName string) error { + // this is to avoid confusing error in case if domain empty for example + // HTTP route will fail producing generic not found error + // instead we catch the error here + if strings.TrimSpace(domainName) == "" { + return trace.BadParameter("empty domain name") + } + _, err := c.Delete(ctx, c.Endpoint("reversetunnels", domainName)) + return trace.Wrap(err) +} diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go index 564359d2d1684..2c99317cb4615 100644 --- a/lib/auth/grpcserver.go +++ b/lib/auth/grpcserver.go @@ -175,15 +175,6 @@ type GRPCServer struct { // TODO(tross) DELETE IN 17.0.0 usersService *usersv1.Service - // botService is used to forward requests to deprecated bot RPCs to the - // new service. - botService *machineidv1.BotService - - // presenceService is used to forward requests to deprecated presence RPCs - // to the new service. - // TODO(noah) DELETE IN 17.0.0 - presenceService *presencev1.Service - // TraceServiceServer exposes the exporter server so that the auth server may // collect and forward spans collectortracepb.TraceServiceServer @@ -5145,6 +5136,7 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) { Authorizer: cfg.Authorizer, AuthServer: cfg.AuthServer, Backend: cfg.AuthServer.Services, + Cache: cfg.AuthServer.Cache, Emitter: cfg.Emitter, Reporter: cfg.AuthServer.Services.UsageReporter, Clock: cfg.AuthServer.GetClock(), @@ -5227,10 +5219,8 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) { Entry: logrus.WithFields(logrus.Fields{ teleport.ComponentKey: teleport.Component(teleport.ComponentAuth, teleport.ComponentGRPC), }), - server: server, - usersService: usersService, - botService: botService, - presenceService: presenceService, + server: server, + usersService: usersService, } if en := os.Getenv("TELEPORT_UNSTABLE_CREATEAUDITSTREAM_INFLIGHT_LIMIT"); en != "" { diff --git a/lib/auth/init.go b/lib/auth/init.go index 0924c26b93f82..7806141983eef 100644 --- a/lib/auth/init.go +++ b/lib/auth/init.go @@ -433,7 +433,7 @@ func initCluster(ctx context.Context, cfg InitConfig, asrv *Server) error { } } for _, tunnel := range cfg.ReverseTunnels { - if err := asrv.UpsertReverseTunnel(tunnel); err != nil { + if err := asrv.UpsertReverseTunnel(ctx, tunnel); err != nil { return trace.Wrap(err) } log.Infof("Created reverse tunnel: %v.", tunnel) diff --git a/lib/auth/presence/presencev1/service.go b/lib/auth/presence/presencev1/service.go index cebb0ba1f6940..8bb5f1511a4ee 100644 --- a/lib/auth/presence/presencev1/service.go +++ b/lib/auth/presence/presencev1/service.go @@ -31,6 +31,7 @@ import ( "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/lib/authz" + "github.com/gravitational/teleport/lib/services" usagereporter "github.com/gravitational/teleport/lib/usagereporter/teleport" "github.com/gravitational/teleport/lib/utils" ) @@ -41,6 +42,13 @@ type Backend interface { ListRemoteClusters(ctx context.Context, pageSize int, nextToken string) ([]types.RemoteCluster, string, error) UpdateRemoteCluster(ctx context.Context, rc types.RemoteCluster) (types.RemoteCluster, error) PatchRemoteCluster(ctx context.Context, name string, updateFn func(rc types.RemoteCluster) (types.RemoteCluster, error)) (types.RemoteCluster, error) + + UpsertReverseTunnelV2(ctx context.Context, tunnel types.ReverseTunnel) (types.ReverseTunnel, error) + DeleteReverseTunnel(ctx context.Context, tunnelName string) error +} + +type Cache interface { + ListReverseTunnels(ctx context.Context, pageSize int, nextToken string) ([]types.ReverseTunnel, string, error) } type AuthServer interface { @@ -56,6 +64,7 @@ type ServiceConfig struct { Authorizer authz.Authorizer AuthServer AuthServer Backend Backend + Cache Cache Logger logrus.FieldLogger Emitter apievents.Emitter Reporter usagereporter.UsageReporter @@ -69,6 +78,7 @@ type Service struct { authorizer authz.Authorizer authServer AuthServer backend Backend + cache Cache logger logrus.FieldLogger emitter apievents.Emitter reporter usagereporter.UsageReporter @@ -88,6 +98,8 @@ func NewService(cfg ServiceConfig) (*Service, error) { return nil, trace.BadParameter("reporter is required") case cfg.AuthServer == nil: return nil, trace.BadParameter("auth server is required") + case cfg.Cache == nil: + return nil, trace.BadParameter("cache is required") } if cfg.Logger == nil { @@ -102,9 +114,11 @@ func NewService(cfg ServiceConfig) (*Service, error) { authorizer: cfg.Authorizer, authServer: cfg.AuthServer, backend: cfg.Backend, - emitter: cfg.Emitter, - reporter: cfg.Reporter, - clock: cfg.Clock, + cache: cfg.Cache, + + emitter: cfg.Emitter, + reporter: cfg.Reporter, + clock: cfg.Clock, }, nil } @@ -291,3 +305,90 @@ func (s *Service) DeleteRemoteCluster( return &emptypb.Empty{}, nil } + +// ListReverseTunnels returns a page of reverse tunnels. +func (s *Service) ListReverseTunnels( + ctx context.Context, req *presencepb.ListReverseTunnelsRequest, +) (*presencepb.ListReverseTunnelsResponse, error) { + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := authCtx.CheckAccessToKind(types.KindReverseTunnel, types.VerbList, types.VerbRead); err != nil { + return nil, trace.Wrap(err) + } + + page, nextToken, err := s.cache.ListReverseTunnels( + ctx, int(req.PageSize), req.PageToken, + ) + if err != nil { + return nil, trace.Wrap(err) + } + + // Convert the reverse tunnels to the concrete type + concretePage := make([]*types.ReverseTunnelV2, 0, len(page)) + for _, rc := range page { + v3, ok := rc.(*types.ReverseTunnelV2) + if !ok { + s.logger.Warnf("expected type ReverseTunnelV2, got %T for %q", rc, rc.GetName()) + continue + } + concretePage = append(concretePage, v3) + } + + return &presencepb.ListReverseTunnelsResponse{ + ReverseTunnels: concretePage, + NextPageToken: nextToken, + }, nil +} + +// UpsertReverseTunnel upserts a reverse tunnel. +func (s *Service) UpsertReverseTunnel( + ctx context.Context, req *presencepb.UpsertReverseTunnelRequest, +) (*types.ReverseTunnelV2, error) { + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := authCtx.CheckAccessToKind(types.KindReverseTunnel, types.VerbCreate, types.VerbUpdate); err != nil { + return nil, trace.Wrap(err) + } + + if req.ReverseTunnel == nil { + return nil, trace.BadParameter("reverse_tunnel: must not be nil") + } + + if err := services.ValidateReverseTunnel(req.ReverseTunnel); err != nil { + return nil, trace.Wrap(err) + } + + res, err := s.backend.UpsertReverseTunnelV2(ctx, req.ReverseTunnel) + if err != nil { + return nil, trace.Wrap(err) + } + concrete, ok := res.(*types.ReverseTunnelV2) + if !ok { + return nil, trace.BadParameter("encountered unexpected reverse tunnel type %T", res) + } + + return concrete, nil +} + +// DeleteReverseTunnel deletes a reverse tunnel. +func (s *Service) DeleteReverseTunnel( + ctx context.Context, req *presencepb.DeleteReverseTunnelRequest, +) (*emptypb.Empty, error) { + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := authCtx.CheckAccessToKind(types.KindReverseTunnel, types.VerbDelete); err != nil { + return nil, trace.Wrap(err) + } + + if req.Name == "" { + return nil, trace.BadParameter("name: must be specified") + } + + return nil, trace.Wrap(s.backend.DeleteReverseTunnel(ctx, req.Name)) +} diff --git a/lib/auth/presence/presencev1/service_test.go b/lib/auth/presence/presencev1/service_test.go index 8f92ed72fec67..2b673b6517a53 100644 --- a/lib/auth/presence/presencev1/service_test.go +++ b/lib/auth/presence/presencev1/service_test.go @@ -21,6 +21,7 @@ package presencev1_test import ( "context" "errors" + "fmt" "net" "os" "testing" @@ -668,3 +669,310 @@ func TestUpdateRemoteCluster(t *testing.T) { }) } } + +// TestListReverseTunnels is an integration test that uses a real gRPC +// client/server. +func TestListReverseTunnels(t *testing.T) { + t.Parallel() + srv := newTestTLSServer(t) + ctx := context.Background() + + user, role, err := auth.CreateUserAndRole( + srv.Auth(), + "rc-getter", + []string{}, + []types.Rule{ + { + Resources: []string{types.KindReverseTunnel}, + Verbs: []string{types.VerbList, types.VerbRead}, + }, + }) + require.NoError(t, err) + _, err = srv.Auth().UpsertRole(ctx, role) + require.NoError(t, err) + + unprivilegedUser, unprivilegedRole, err := auth.CreateUserAndRole( + srv.Auth(), + "no-perms", + []string{}, + []types.Rule{}, + ) + require.NoError(t, err) + unprivilegedRole.SetRules(types.Deny, []types.Rule{ + { + Resources: []string{types.KindReverseTunnel}, + Verbs: []string{types.VerbList}, + }, + }) + _, err = srv.Auth().UpsertRole(ctx, unprivilegedRole) + require.NoError(t, err) + + // Create a few reverse tunnels + created := []*types.ReverseTunnelV2{} + for i := 0; i < 10; i++ { + rc, err := types.NewReverseTunnel(fmt.Sprintf("rt-%d", i), []string{"example.com:443"}) + require.NoError(t, err) + err = srv.Auth().Services.UpsertReverseTunnel(ctx, rc) + require.NoError(t, err) + created = append(created, rc.(*types.ReverseTunnelV2)) + } + + tests := []struct { + name string + user string + req *presencev1pb.ListReverseTunnelsRequest + assertError require.ErrorAssertionFunc + want *presencev1pb.ListReverseTunnelsResponse + }{ + { + name: "success", + user: user.GetName(), + req: &presencev1pb.ListReverseTunnelsRequest{}, + assertError: require.NoError, + want: &presencev1pb.ListReverseTunnelsResponse{ + ReverseTunnels: created, + }, + }, + { + name: "no permissions", + user: unprivilegedUser.GetName(), + req: &presencev1pb.ListReverseTunnelsRequest{}, + assertError: func(t require.TestingT, err error, i ...interface{}) { + require.True(t, trace.IsAccessDenied(err), "error should be access denied") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := srv.NewClient(auth.TestUser(tt.user)) + require.NoError(t, err) + + res, err := client.PresenceServiceClient().ListReverseTunnels(ctx, tt.req) + tt.assertError(t, err) + if tt.want != nil { + // Check that the returned data matches + require.Empty( + t, cmp.Diff( + tt.want, + res, + protocmp.Transform(), + protocmp.SortRepeatedFields(&presencev1pb.ListReverseTunnelsResponse{}, "reverse_tunnels"), + ), + ) + } + }) + } + + t.Run("pagination", func(t *testing.T) { + client, err := srv.NewClient(auth.TestUser(user.GetName())) + require.NoError(t, err) + + allGot := []*types.ReverseTunnelV2{} + pageToken := "" + for i := 0; i < 10; i++ { + var got []types.ReverseTunnel + got, pageToken, err = client.ListReverseTunnels(ctx, 1, pageToken) + require.NoError(t, err) + if i == 9 { + // For the final page, we should not get a page token + require.Empty(t, pageToken) + } else { + require.NotEmpty(t, pageToken) + } + require.Len(t, got, 1) + for _, item := range got { + allGot = append(allGot, item.(*types.ReverseTunnelV2)) + } + } + require.Len(t, allGot, 10) + + // Check that the returned data matches + require.Empty( + t, cmp.Diff( + allGot, + created), + ) + }) +} + +// TestDeleteReverseTunnel is an integration test that uses a real gRPC client/server. +func TestDeleteReverseTunnel(t *testing.T) { + t.Parallel() + srv := newTestTLSServer(t) + ctx := context.Background() + + user, _, err := auth.CreateUserAndRole( + srv.Auth(), + "rt-deleter", + []string{}, + []types.Rule{ + { + Resources: []string{types.KindReverseTunnel}, + Verbs: []string{types.VerbDelete}, + }, + }) + require.NoError(t, err) + unprivilegedUser, _, err := auth.CreateUserAndRole( + srv.Auth(), + "no-perms", + []string{}, + []types.Rule{}, + ) + require.NoError(t, err) + + rt, err := types.NewReverseTunnel("example.com", []string{"example.com:443"}) + require.NoError(t, err) + rt, err = srv.Auth().UpsertReverseTunnelV2(ctx, rt) + require.NoError(t, err) + + tests := []struct { + name string + user string + req *presencev1pb.DeleteReverseTunnelRequest + assertError require.ErrorAssertionFunc + checkResourcesDeleted bool + }{ + { + name: "success", + user: user.GetName(), + req: &presencev1pb.DeleteReverseTunnelRequest{ + Name: rt.GetName(), + }, + assertError: require.NoError, + checkResourcesDeleted: true, + }, + { + name: "no permissions", + user: unprivilegedUser.GetName(), + req: &presencev1pb.DeleteReverseTunnelRequest{ + Name: rt.GetName(), + }, + assertError: func(t require.TestingT, err error, i ...interface{}) { + require.True(t, trace.IsAccessDenied(err), "error should be access denied") + }, + }, + { + name: "non existent", + user: user.GetName(), + req: &presencev1pb.DeleteReverseTunnelRequest{ + Name: rt.GetName(), + }, + assertError: func(t require.TestingT, err error, i ...interface{}) { + require.True(t, trace.IsNotFound(err), "error should be not found") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := srv.NewClient(auth.TestUser(tt.user)) + require.NoError(t, err) + + _, err = client.PresenceServiceClient().DeleteReverseTunnel(ctx, tt.req) + tt.assertError(t, err) + if tt.checkResourcesDeleted { + _, err := srv.Auth().GetReverseTunnel(ctx, tt.req.Name) + require.True(t, trace.IsNotFound(err), "rt should be deleted") + } + }) + } +} + +func TestUpsertReverseTunnel(t *testing.T) { + t.Parallel() + srv := newTestTLSServer(t) + ctx := context.Background() + + user, _, err := auth.CreateUserAndRole( + srv.Auth(), + "rt-upserter", + []string{}, + []types.Rule{ + { + Resources: []string{types.KindReverseTunnel}, + Verbs: []string{types.VerbCreate, types.VerbUpdate}, + }, + }) + require.NoError(t, err) + unprivilegedUser, _, err := auth.CreateUserAndRole( + srv.Auth(), + "no-perms", + []string{}, + []types.Rule{}, + ) + require.NoError(t, err) + + rt, err := types.NewReverseTunnel("example.com", []string{"example.com:443"}) + require.NoError(t, err) + + invalid, err := types.NewReverseTunnel("example.com", []string{"!!://///example.com:44/./3!!!"}) + require.NoError(t, err) + + tests := []struct { + name string + user string + req *presencev1pb.UpsertReverseTunnelRequest + assertError require.ErrorAssertionFunc + want *types.ReverseTunnelV2 + }{ + { + name: "success", + user: user.GetName(), + req: &presencev1pb.UpsertReverseTunnelRequest{ + ReverseTunnel: rt.(*types.ReverseTunnelV2), + }, + assertError: require.NoError, + want: rt.(*types.ReverseTunnelV2), + }, + { + name: "no permissions", + user: unprivilegedUser.GetName(), + req: &presencev1pb.UpsertReverseTunnelRequest{ + ReverseTunnel: rt.(*types.ReverseTunnelV2), + }, + assertError: func(t require.TestingT, err error, i ...interface{}) { + require.True(t, trace.IsAccessDenied(err), "error should be access denied") + }, + }, + { + name: "no value", + user: user.GetName(), + req: &presencev1pb.UpsertReverseTunnelRequest{ + ReverseTunnel: nil, + }, + assertError: func(t require.TestingT, err error, i ...interface{}) { + require.True(t, trace.IsBadParameter(err), "error should be bad parameter") + }, + }, + { + name: "validation - invalid", + user: user.GetName(), + req: &presencev1pb.UpsertReverseTunnelRequest{ + ReverseTunnel: invalid.(*types.ReverseTunnelV2), + }, + assertError: func(t require.TestingT, err error, i ...interface{}) { + require.ErrorContains(t, err, "failed to parse") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := srv.NewClient(auth.TestUser(tt.user)) + require.NoError(t, err) + + got, err := client.PresenceServiceClient().UpsertReverseTunnel(ctx, tt.req) + tt.assertError(t, err) + if tt.want != nil { + // Check that the returned rt matches + require.Empty( + t, + cmp.Diff( + tt.want, + got, + cmpopts.IgnoreFields(types.Metadata{}, "Revision"), + ), + ) + } + }) + } + +} diff --git a/lib/auth/trustedcluster.go b/lib/auth/trustedcluster.go index 500a367010f6a..323c27bbb5f13 100644 --- a/lib/auth/trustedcluster.go +++ b/lib/auth/trustedcluster.go @@ -107,7 +107,7 @@ func (a *Server) UpsertTrustedCluster(ctx context.Context, trustedCluster types. return nil, trace.Wrap(err) } - if err := a.createReverseTunnel(trustedCluster); err != nil { + if err := a.createReverseTunnel(ctx, trustedCluster); err != nil { return nil, trace.Wrap(err) } case existingCluster != nil && enable == false: @@ -123,7 +123,7 @@ func (a *Server) UpsertTrustedCluster(ctx context.Context, trustedCluster types. return nil, trace.Wrap(err) } - if err := a.DeleteReverseTunnel(trustedCluster.GetName()); err != nil { + if err := a.DeleteReverseTunnel(ctx, trustedCluster.GetName()); err != nil { return nil, trace.Wrap(err) } case existingCluster == nil && enable == true: @@ -142,7 +142,7 @@ func (a *Server) UpsertTrustedCluster(ctx context.Context, trustedCluster types. return nil, trace.Wrap(err) } - if err := a.createReverseTunnel(trustedCluster); err != nil { + if err := a.createReverseTunnel(ctx, trustedCluster); err != nil { return nil, trace.Wrap(err) } @@ -234,7 +234,7 @@ func (a *Server) DeleteTrustedCluster(ctx context.Context, name string) error { return trace.Wrap(err) } - if err := a.DeleteReverseTunnel(name); err != nil { + if err := a.DeleteReverseTunnel(ctx, name); err != nil { if !trace.IsNotFound(err) { return trace.Wrap(err) } @@ -674,7 +674,7 @@ func (a *Server) deactivateCertAuthority(ctx context.Context, t types.TrustedClu // createReverseTunnel will create a services.ReverseTunnel givenin the // services.TrustedCluster resource. -func (a *Server) createReverseTunnel(t types.TrustedCluster) error { +func (a *Server) createReverseTunnel(ctx context.Context, t types.TrustedCluster) error { reverseTunnel, err := types.NewReverseTunnel( t.GetName(), []string{t.GetReverseTunnelAddress()}, @@ -682,5 +682,5 @@ func (a *Server) createReverseTunnel(t types.TrustedCluster) error { if err != nil { return trace.Wrap(err) } - return trace.Wrap(a.UpsertReverseTunnel(reverseTunnel)) + return trace.Wrap(a.UpsertReverseTunnel(ctx, reverseTunnel)) } diff --git a/lib/auth/trustedcluster_test.go b/lib/auth/trustedcluster_test.go index ee7f52c7a89ae..ba7ffac769b62 100644 --- a/lib/auth/trustedcluster_test.go +++ b/lib/auth/trustedcluster_test.go @@ -487,7 +487,7 @@ func TestUpsertTrustedCluster(t *testing.T) { err = a.UpsertCertAuthority(ctx, ca) require.NoError(t, err) - err = a.createReverseTunnel(trustedCluster) + err = a.createReverseTunnel(ctx, trustedCluster) require.NoError(t, err) t.Run("Invalid role change", func(t *testing.T) { diff --git a/lib/cache/cache.go b/lib/cache/cache.go index cc437c2d7c0e5..d0f0b467e1703 100644 --- a/lib/cache/cache.go +++ b/lib/cache/cache.go @@ -2158,19 +2158,6 @@ func (c *Cache) GetAuthServers() ([]types.Server, error) { return rg.reader.GetAuthServers() } -// GetReverseTunnels is a part of auth.Cache implementation -func (c *Cache) GetReverseTunnels(ctx context.Context, opts ...services.MarshalOption) ([]types.ReverseTunnel, error) { - ctx, span := c.Tracer.Start(ctx, "cache/GetReverseTunnels") - defer span.End() - - rg, err := readCollectionCache(c, c.collections.reverseTunnels) - if err != nil { - return nil, trace.Wrap(err) - } - defer rg.Release() - return rg.reader.GetReverseTunnels(ctx, opts...) -} - // GetProxies is a part of auth.Cache implementation func (c *Cache) GetProxies() ([]types.Server, error) { _, span := c.Tracer.Start(context.TODO(), "cache/GetProxies") diff --git a/lib/cache/cache_test.go b/lib/cache/cache_test.go index 827bfc154cbca..4292b93fb3b66 100644 --- a/lib/cache/cache_test.go +++ b/lib/cache/cache_test.go @@ -1659,32 +1659,6 @@ func TestRoles(t *testing.T) { }) } -// TestReverseTunnels tests reverse tunnels caching -func TestReverseTunnels(t *testing.T) { - t.Parallel() - - p, err := newPack(t.TempDir(), ForProxy) - require.NoError(t, err) - t.Cleanup(p.Close) - - testResources(t, p, testFuncs[types.ReverseTunnel]{ - newResource: func(name string) (types.ReverseTunnel, error) { - return types.NewReverseTunnel(name, []string{"example.com:2023"}) - }, - create: modifyNoContext(p.presenceS.UpsertReverseTunnel), - list: func(ctx context.Context) ([]types.ReverseTunnel, error) { - return p.presenceS.GetReverseTunnels(ctx) - }, - cacheList: func(ctx context.Context) ([]types.ReverseTunnel, error) { - return p.cache.GetReverseTunnels(ctx) - }, - update: modifyNoContext(p.presenceS.UpsertReverseTunnel), - deleteAll: func(ctx context.Context) error { - return p.presenceS.DeleteAllReverseTunnels() - }, - }) -} - // TestTunnelConnections tests tunnel connections caching func TestTunnelConnections(t *testing.T) { t.Parallel() diff --git a/lib/cache/collections.go b/lib/cache/collections.go index 236f58af73b0d..a03a786cd41f2 100644 --- a/lib/cache/collections.go +++ b/lib/cache/collections.go @@ -953,39 +953,6 @@ type remoteClusterGetter interface { var _ executor[types.RemoteCluster, remoteClusterGetter] = remoteClusterExecutor{} -type reverseTunnelExecutor struct{} - -func (reverseTunnelExecutor) getAll(ctx context.Context, cache *Cache, loadSecrets bool) ([]types.ReverseTunnel, error) { - return cache.Presence.GetReverseTunnels(ctx) -} - -func (reverseTunnelExecutor) upsert(ctx context.Context, cache *Cache, resource types.ReverseTunnel) error { - return cache.presenceCache.UpsertReverseTunnel(resource) -} - -func (reverseTunnelExecutor) deleteAll(ctx context.Context, cache *Cache) error { - return cache.presenceCache.DeleteAllReverseTunnels() -} - -func (reverseTunnelExecutor) delete(ctx context.Context, cache *Cache, resource types.Resource) error { - return cache.presenceCache.DeleteReverseTunnel(resource.GetName()) -} - -func (reverseTunnelExecutor) isSingleton() bool { return false } - -func (reverseTunnelExecutor) getReader(cache *Cache, cacheOK bool) reverseTunnelGetter { - if cacheOK { - return cache.presenceCache - } - return cache.Config.Presence -} - -type reverseTunnelGetter interface { - GetReverseTunnels(ctx context.Context, opts ...services.MarshalOption) ([]types.ReverseTunnel, error) -} - -var _ executor[types.ReverseTunnel, reverseTunnelGetter] = reverseTunnelExecutor{} - type proxyExecutor struct{} func (proxyExecutor) getAll(ctx context.Context, cache *Cache, loadSecrets bool) ([]types.Server, error) { diff --git a/lib/cache/resource_reverse_tunnel.go b/lib/cache/resource_reverse_tunnel.go new file mode 100644 index 0000000000000..34e963ca970c0 --- /dev/null +++ b/lib/cache/resource_reverse_tunnel.go @@ -0,0 +1,87 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//nolint:unused // Because the executors generate a large amount of false positives. +package cache + +import ( + "context" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" +) + +// GetReverseTunnels is a part of auth.Cache implementation +// Deprecated: use ListReverseTunnels +func (c *Cache) GetReverseTunnels(ctx context.Context) ([]types.ReverseTunnel, error) { + ctx, span := c.Tracer.Start(ctx, "cache/GetReverseTunnels") + defer span.End() + + rg, err := readCollectionCache(c, c.collections.reverseTunnels) + if err != nil { + return nil, trace.Wrap(err) + } + defer rg.Release() + return rg.reader.GetReverseTunnels(ctx) +} + +// ListReverseTunnels is a part of auth.Cache implementation +func (c *Cache) ListReverseTunnels(ctx context.Context, pageSize int, pageToken string) ([]types.ReverseTunnel, string, error) { + ctx, span := c.Tracer.Start(ctx, "cache/ListReverseTunnels") + defer span.End() + + rg, err := readCollectionCache(c, c.collections.reverseTunnels) + if err != nil { + return nil, "", trace.Wrap(err) + } + defer rg.Release() + return rg.reader.ListReverseTunnels(ctx, pageSize, pageToken) +} + +type reverseTunnelGetter interface { + GetReverseTunnels(ctx context.Context) ([]types.ReverseTunnel, error) + ListReverseTunnels(ctx context.Context, pageSize int, pageToken string) ([]types.ReverseTunnel, string, error) +} + +var _ executor[types.ReverseTunnel, reverseTunnelGetter] = reverseTunnelExecutor{} + +type reverseTunnelExecutor struct{} + +func (reverseTunnelExecutor) getAll(ctx context.Context, cache *Cache, loadSecrets bool) ([]types.ReverseTunnel, error) { + return cache.Presence.GetReverseTunnels(ctx) +} + +func (reverseTunnelExecutor) upsert(ctx context.Context, cache *Cache, resource types.ReverseTunnel) error { + return cache.presenceCache.UpsertReverseTunnel(ctx, resource) +} + +func (reverseTunnelExecutor) deleteAll(ctx context.Context, cache *Cache) error { + return cache.presenceCache.DeleteAllReverseTunnels(ctx) +} + +func (reverseTunnelExecutor) delete(ctx context.Context, cache *Cache, resource types.Resource) error { + return cache.presenceCache.DeleteReverseTunnel(ctx, resource.GetName()) +} + +func (reverseTunnelExecutor) isSingleton() bool { return false } + +func (reverseTunnelExecutor) getReader(cache *Cache, cacheOK bool) reverseTunnelGetter { + if cacheOK { + return cache.presenceCache + } + return cache.Config.Presence +} diff --git a/lib/cache/resource_reverse_tunnel_test.go b/lib/cache/resource_reverse_tunnel_test.go new file mode 100644 index 0000000000000..a228cb3be4822 --- /dev/null +++ b/lib/cache/resource_reverse_tunnel_test.go @@ -0,0 +1,50 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cache + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/types" +) + +// TestReverseTunnels tests reverse tunnels caching +func TestReverseTunnels(t *testing.T) { + t.Parallel() + + p, err := newPack(t.TempDir(), ForProxy) + require.NoError(t, err) + t.Cleanup(p.Close) + + testResources(t, p, testFuncs[types.ReverseTunnel]{ + newResource: func(name string) (types.ReverseTunnel, error) { + return types.NewReverseTunnel(name, []string{"example.com:2023"}) + }, + create: p.presenceS.UpsertReverseTunnel, + list: func(ctx context.Context) ([]types.ReverseTunnel, error) { + return p.presenceS.GetReverseTunnels(ctx) + }, + cacheList: func(ctx context.Context) ([]types.ReverseTunnel, error) { + return p.cache.GetReverseTunnels(ctx) + }, + update: p.presenceS.UpsertReverseTunnel, + deleteAll: p.presenceS.DeleteAllReverseTunnels, + }) +} diff --git a/lib/reversetunnel/rc_manager_test.go b/lib/reversetunnel/rc_manager_test.go index 91a45c9db3fbb..9fd1399ae7950 100644 --- a/lib/reversetunnel/rc_manager_test.go +++ b/lib/reversetunnel/rc_manager_test.go @@ -30,7 +30,6 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/reversetunnelclient" - "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" ) @@ -189,7 +188,7 @@ type mockAuthClient struct { reverseTunnelsErr error } -func (c mockAuthClient) GetReverseTunnels(context.Context, ...services.MarshalOption) ([]types.ReverseTunnel, error) { +func (c mockAuthClient) GetReverseTunnels(context.Context) ([]types.ReverseTunnel, error) { return c.reverseTunnels, c.reverseTunnelsErr } diff --git a/lib/services/local/presence.go b/lib/services/local/presence.go index dbc03946e20a9..4cc4bee70cbc0 100644 --- a/lib/services/local/presence.go +++ b/lib/services/local/presence.go @@ -20,6 +20,7 @@ package local import ( "context" + "log/slog" "sort" "time" @@ -417,42 +418,61 @@ func (s *PresenceService) DeleteProxy(ctx context.Context, name string) error { } // DeleteAllReverseTunnels deletes all reverse tunnels -func (s *PresenceService) DeleteAllReverseTunnels() error { +func (s *PresenceService) DeleteAllReverseTunnels(ctx context.Context) error { startKey := backend.ExactKey(reverseTunnelsPrefix) - return s.DeleteRange(context.TODO(), startKey, backend.RangeEnd(startKey)) + return s.DeleteRange(ctx, startKey, backend.RangeEnd(startKey)) +} + +// UpsertReverseTunnel upserts reverse tunnel entry +func (s *PresenceService) UpsertReverseTunnel(ctx context.Context, tunnel types.ReverseTunnel) error { + _, err := s.UpsertReverseTunnelV2(ctx, tunnel) + return trace.Wrap(err) } -// UpsertReverseTunnel upserts reverse tunnel entry temporarily or permanently -func (s *PresenceService) UpsertReverseTunnel(tunnel types.ReverseTunnel) error { +// UpsertReverseTunnelV2 upserts reverse tunnel entry and returns the upserted +// value. +// TODO(noah): In v18, we can rename this to UpsertReverseTunnel and remove the +// version which does not return the upserted value. +func (s *PresenceService) UpsertReverseTunnelV2(ctx context.Context, tunnel types.ReverseTunnel) (types.ReverseTunnel, error) { if err := services.ValidateReverseTunnel(tunnel); err != nil { - return trace.Wrap(err) + return nil, trace.Wrap(err) } rev := tunnel.GetRevision() value, err := services.MarshalReverseTunnel(tunnel) if err != nil { - return trace.Wrap(err) + return nil, trace.Wrap(err) } - _, err = s.Put(context.TODO(), backend.Item{ + lease, err := s.Put(ctx, backend.Item{ Key: backend.NewKey(reverseTunnelsPrefix, tunnel.GetName()), Value: value, Expires: tunnel.Expiry(), Revision: rev, }) - return trace.Wrap(err) + if err != nil { + return nil, trace.Wrap(err) + } + + tunnel.SetRevision(lease.Revision) + return tunnel, nil } // GetReverseTunnel returns reverse tunnel by name -func (s *PresenceService) GetReverseTunnel(name string, opts ...services.MarshalOption) (types.ReverseTunnel, error) { - item, err := s.Get(context.TODO(), backend.NewKey(reverseTunnelsPrefix, name)) +func (s *PresenceService) GetReverseTunnel(ctx context.Context, name string) (types.ReverseTunnel, error) { + item, err := s.Get(ctx, backend.NewKey(reverseTunnelsPrefix, name)) if err != nil { return nil, trace.Wrap(err) } - return services.UnmarshalReverseTunnel(item.Value, - services.AddOptions(opts, services.WithExpires(item.Expires), services.WithRevision(item.Revision))...) + return services.UnmarshalReverseTunnel( + item.Value, + services.WithExpires(item.Expires), + services.WithRevision(item.Revision), + ) } // GetReverseTunnels returns a list of registered servers -func (s *PresenceService) GetReverseTunnels(ctx context.Context, opts ...services.MarshalOption) ([]types.ReverseTunnel, error) { +// Deprecated: use ListReverseTunnels +// TODO(noah): REMOVE IN 18.0.0 - replace with calls to ListReverseTunnels +func (s *PresenceService) GetReverseTunnels(ctx context.Context) ([]types.ReverseTunnel, error) { startKey := backend.ExactKey(reverseTunnelsPrefix) result, err := s.GetRange(ctx, startKey, backend.RangeEnd(startKey), backend.NoLimit) if err != nil { @@ -464,7 +484,10 @@ func (s *PresenceService) GetReverseTunnels(ctx context.Context, opts ...service } for i, item := range result.Items { tunnel, err := services.UnmarshalReverseTunnel( - item.Value, services.AddOptions(opts, services.WithExpires(item.Expires), services.WithRevision(item.Revision))...) + item.Value, + services.WithExpires(item.Expires), + services.WithRevision(item.Revision), + ) if err != nil { return nil, trace.Wrap(err) } @@ -476,11 +499,53 @@ func (s *PresenceService) GetReverseTunnels(ctx context.Context, opts ...service } // DeleteReverseTunnel deletes reverse tunnel by it's cluster name -func (s *PresenceService) DeleteReverseTunnel(clusterName string) error { - err := s.Delete(context.TODO(), backend.NewKey(reverseTunnelsPrefix, clusterName)) +func (s *PresenceService) DeleteReverseTunnel(ctx context.Context, clusterName string) error { + err := s.Delete(ctx, backend.NewKey(reverseTunnelsPrefix, clusterName)) return trace.Wrap(err) } +// ListReverseTunnels returns a paginated list of reverse tunnels +func (s *PresenceService) ListReverseTunnels( + ctx context.Context, pageSize int, pageToken string, +) ([]types.ReverseTunnel, string, error) { + rangeStart := backend.NewKey(reverseTunnelsPrefix, pageToken) + rangeEnd := backend.RangeEnd(backend.ExactKey(reverseTunnelsPrefix)) + + // Adjust page size, so it can't be too large. + if pageSize <= 0 || pageSize > apidefaults.DefaultChunkSize { + pageSize = apidefaults.DefaultChunkSize + } + + limit := pageSize + 1 + + result, err := s.GetRange(ctx, rangeStart, rangeEnd, limit) + if err != nil { + return nil, "", trace.Wrap(err) + } + + tunnels := make([]types.ReverseTunnel, 0, len(result.Items)) + for _, item := range result.Items { + tunnel, err := services.UnmarshalReverseTunnel(item.Value, + services.WithExpires(item.Expires), + services.WithRevision(item.Revision), + ) + if err != nil { + slog.WarnContext(ctx, "Skipping item during ListReverseTunnels because conversion from backend item failed", "key", item.Key, "error", err) + continue + } + tunnels = append(tunnels, tunnel) + } + + next := "" + if len(tunnels) > pageSize { + next = backend.GetPaginationKey(tunnels[pageSize]) + clear(tunnels[pageSize:]) + // Truncate the last item that was used to determine next row existence. + tunnels = tunnels[:pageSize] + } + return tunnels, next, nil +} + // this combination of backoff parameters leads to worst-case total time spent // in backoff between 1ms and 2000ms depending on jitter. tests are in // place to verify that this is sufficient to resolve a 20-lease contention diff --git a/lib/services/local/presence_test.go b/lib/services/local/presence_test.go index 96bcb34ff2258..a49fcafc65100 100644 --- a/lib/services/local/presence_test.go +++ b/lib/services/local/presence_test.go @@ -38,6 +38,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/backend/lite" + "github.com/gravitational/teleport/lib/backend/memory" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/services/suite" ) @@ -1275,3 +1276,85 @@ func TestServerInfoCRUD(t *testing.T) { require.NoError(t, err) require.Empty(t, out) } + +func TestPresenceService_ListReverseTunnels(t *testing.T) { + t.Parallel() + ctx := context.Background() + + bk, err := memory.New(memory.Config{}) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, bk.Close()) }) + + presenceService := NewPresenceService(bk) + + // With no resources, we should not get an error but we should get an empty + // token and an empty slice. + rcs, pageToken, err := presenceService.ListReverseTunnels(ctx, 0, "") + require.NoError(t, err) + require.Empty(t, pageToken) + require.Empty(t, rcs) + + // Create a few remote clusters + for i := 0; i < 10; i++ { + rc, err := types.NewReverseTunnel(fmt.Sprintf("rt-%d", i), []string{"example.com:443"}) + require.NoError(t, err) + err = presenceService.UpsertReverseTunnel(ctx, rc) + require.NoError(t, err) + } + + // Check limit behaves + rcs, pageToken, err = presenceService.ListReverseTunnels(ctx, 1, "") + require.NoError(t, err) + require.NotEmpty(t, pageToken) + require.Len(t, rcs, 1) + + // Iterate through all pages with a low limit to ensure that pageToken + // behaves correctly. + rcs = []types.ReverseTunnel{} + pageToken = "" + for i := 0; i < 10; i++ { + var got []types.ReverseTunnel + got, pageToken, err = presenceService.ListReverseTunnels(ctx, 1, pageToken) + require.NoError(t, err) + if i == 9 { + // For the final page, we should not get a page token + require.Empty(t, pageToken) + } else { + require.NotEmpty(t, pageToken) + } + require.Len(t, got, 1) + rcs = append(rcs, got...) + } + require.Len(t, rcs, 10) + + // Check that with a higher limit, we get all resources + rcs, pageToken, err = presenceService.ListReverseTunnels(ctx, 20, "") + require.NoError(t, err) + require.Empty(t, pageToken) + require.Len(t, rcs, 10) +} + +func TestPresenceService_UpsertReverseTunnel(t *testing.T) { + t.Parallel() + ctx := context.Background() + + bk, err := memory.New(memory.Config{}) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, bk.Close()) }) + + presenceService := NewPresenceService(bk) + + rt, err := types.NewReverseTunnel("my-tunnel", []string{"example.com:443"}) + require.NoError(t, err) + + // Upsert a reverse tunnel + got, err := presenceService.UpsertReverseTunnelV2(ctx, rt) + require.NoError(t, err) + // Check that the returned resource is the same as the one we upserted + require.Empty(t, cmp.Diff(rt, got, cmpopts.IgnoreFields(types.Metadata{}, "Revision"))) + + // Do a get to check revision matches + fetched, err := presenceService.GetReverseTunnel(ctx, rt.GetName()) + require.NoError(t, err) + require.Empty(t, cmp.Diff(got, fetched)) +} diff --git a/lib/services/presence.go b/lib/services/presence.go index 1b7528ebdf425..d69bfb8eed6d0 100644 --- a/lib/services/presence.go +++ b/lib/services/presence.go @@ -108,19 +108,23 @@ type Presence interface { DeleteAllProxies() error // UpsertReverseTunnel upserts reverse tunnel entry temporarily or permanently - UpsertReverseTunnel(tunnel types.ReverseTunnel) error + UpsertReverseTunnel(ctx context.Context, tunnel types.ReverseTunnel) error // GetReverseTunnel returns reverse tunnel by name - GetReverseTunnel(name string, opts ...MarshalOption) (types.ReverseTunnel, error) + GetReverseTunnel(ctx context.Context, name string) (types.ReverseTunnel, error) // GetReverseTunnels returns a list of registered servers - GetReverseTunnels(ctx context.Context, opts ...MarshalOption) ([]types.ReverseTunnel, error) + // Deprecated: use ListReverseTunnels + GetReverseTunnels(ctx context.Context) ([]types.ReverseTunnel, error) // DeleteReverseTunnel deletes reverse tunnel by it's domain name - DeleteReverseTunnel(domainName string) error + DeleteReverseTunnel(ctx context.Context, domainName string) error // DeleteAllReverseTunnels deletes all reverse tunnels - DeleteAllReverseTunnels() error + DeleteAllReverseTunnels(ctx context.Context) error + + // ListReverseTunnels returns a page of ReverseTunnels. + ListReverseTunnels(ctx context.Context, pageSize int, pageToken string) ([]types.ReverseTunnel, string, error) // GetNamespaces returns a list of namespaces GetNamespaces() ([]types.Namespace, error) @@ -207,4 +211,5 @@ type PresenceInternal interface { UpsertHostUserInteractionTime(ctx context.Context, name string, loginTime time.Time) error GetHostUserInteractionTime(ctx context.Context, name string) (time.Time, error) + UpsertReverseTunnelV2(ctx context.Context, tunnel types.ReverseTunnel) (types.ReverseTunnel, error) } diff --git a/lib/services/suite/suite.go b/lib/services/suite/suite.go index c749d5c63a75c..880005063464d 100644 --- a/lib/services/suite/suite.go +++ b/lib/services/suite/suite.go @@ -600,32 +600,34 @@ func newReverseTunnel(clusterName string, dialAddrs []string) *types.ReverseTunn } func (s *ServicesTestSuite) ReverseTunnelsCRUD(t *testing.T) { - out, err := s.PresenceS.GetReverseTunnels(context.Background()) + ctx := context.Background() + + out, err := s.PresenceS.GetReverseTunnels(ctx) require.NoError(t, err) require.Empty(t, out) tunnel := newReverseTunnel("example.com", []string{"example.com:2023"}) - require.NoError(t, s.PresenceS.UpsertReverseTunnel(tunnel)) + require.NoError(t, s.PresenceS.UpsertReverseTunnel(ctx, tunnel)) - out, err = s.PresenceS.GetReverseTunnels(context.Background()) + out, err = s.PresenceS.GetReverseTunnels(ctx) require.NoError(t, err) require.Len(t, out, 1) require.Empty(t, cmp.Diff(out, []types.ReverseTunnel{tunnel}, cmpopts.IgnoreFields(types.Metadata{}, "Revision"))) - err = s.PresenceS.DeleteReverseTunnel(tunnel.Spec.ClusterName) + err = s.PresenceS.DeleteReverseTunnel(ctx, tunnel.Spec.ClusterName) require.NoError(t, err) - out, err = s.PresenceS.GetReverseTunnels(context.Background()) + out, err = s.PresenceS.GetReverseTunnels(ctx) require.NoError(t, err) require.Empty(t, out) - err = s.PresenceS.UpsertReverseTunnel(newReverseTunnel("", []string{"127.0.0.1:1234"})) + err = s.PresenceS.UpsertReverseTunnel(ctx, newReverseTunnel("", []string{"127.0.0.1:1234"})) require.True(t, trace.IsBadParameter(err)) - err = s.PresenceS.UpsertReverseTunnel(newReverseTunnel("example.com", []string{""})) + err = s.PresenceS.UpsertReverseTunnel(ctx, newReverseTunnel("example.com", []string{""})) require.True(t, trace.IsBadParameter(err)) - err = s.PresenceS.UpsertReverseTunnel(newReverseTunnel("example.com", []string{})) + err = s.PresenceS.UpsertReverseTunnel(ctx, newReverseTunnel("example.com", []string{})) require.True(t, trace.IsBadParameter(err)) } @@ -1883,12 +1885,12 @@ func (s *ServicesTestSuite) Events(t *testing.T) { }, crud: func(context.Context) types.Resource { tunnel := newReverseTunnel("example.com", []string{"example.com:2023"}) - require.NoError(t, s.PresenceS.UpsertReverseTunnel(tunnel)) + require.NoError(t, s.PresenceS.UpsertReverseTunnel(ctx, tunnel)) out, err := s.PresenceS.GetReverseTunnels(context.Background()) require.NoError(t, err) - err = s.PresenceS.DeleteReverseTunnel(tunnel.Spec.ClusterName) + err = s.PresenceS.DeleteReverseTunnel(ctx, tunnel.Spec.ClusterName) require.NoError(t, err) return out[0] diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index e59bd4bc6a1ec..71e1ad1cf09d6 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -1549,7 +1549,7 @@ func (rc *ResourceCommand) Delete(ctx context.Context, client *authclient.Client } fmt.Printf("github connector %q has been deleted\n", rc.ref.Name) case types.KindReverseTunnel: - if err := client.DeleteReverseTunnel(rc.ref.Name); err != nil { + if err := client.DeleteReverseTunnel(ctx, rc.ref.Name); err != nil { return trace.Wrap(err) } fmt.Printf("reverse tunnel %v has been deleted\n", rc.ref.Name)