Files
teleport/lib/teleterm/daemon/daemon.go
T
Rafał Cieślak f43297f56a Connect: Remove leftovers from resource cache removal (#22772)
* Connect: Remove leftovers from resource cache removal

* GetAll* RPCs were used in the resource cache and are no longer used.
* ListApps was pretty much never actually used as we didn't ever list apps
  in Connect.
* ClusterContext.state.searchValue ceased to be used after we moved to
  fetching resources from the backend.

* Remove remaining leftovers from tshd client mock

* Remove unused imports
2023-03-10 10:05:18 +00:00

578 lines
17 KiB
Go

// Copyright 2021 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package daemon
import (
"context"
"sync"
"time"
"github.com/gravitational/trace"
"google.golang.org/grpc"
"github.com/gravitational/teleport/api/types"
api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1"
"github.com/gravitational/teleport/lib/client/db/dbcmd"
"github.com/gravitational/teleport/lib/teleterm/clusters"
"github.com/gravitational/teleport/lib/teleterm/gateway"
usagereporter "github.com/gravitational/teleport/lib/usagereporter/daemon"
)
const (
// tshdEventsTimeout is the maximum amount of time the gRPC client managed by the tshd daemon will
// wait for a response from the tshd events server managed by the Electron app. This timeout
// should be used for quick one-off calls where the client doesn't need the server or the user to
// perform any additional work, such as the SendNotification RPC.
tshdEventsTimeout = time.Second
)
// New creates an instance of Daemon service
func New(cfg Config) (*Service, error) {
if err := cfg.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
closeContext, cancel := context.WithCancel(context.Background())
connectUsageReporter, err := usagereporter.NewConnectUsageReporter(closeContext, cfg.PrehogAddr)
if err != nil {
cancel()
return nil, trace.Wrap(err)
}
go connectUsageReporter.Run(closeContext)
return &Service{
cfg: &cfg,
closeContext: closeContext,
cancel: cancel,
gateways: make(map[string]*gateway.Gateway),
usageReporter: connectUsageReporter,
}, nil
}
// ListRootClusters returns a list of root clusters
func (s *Service) ListRootClusters(ctx context.Context) ([]*clusters.Cluster, error) {
clusters, err := s.cfg.Storage.ReadAll()
if err != nil {
return nil, trace.Wrap(err)
}
return clusters, nil
}
// ListLeafClusters returns a list of leaf clusters
func (s *Service) ListLeafClusters(ctx context.Context, uri string) ([]clusters.LeafCluster, error) {
cluster, err := s.ResolveCluster(uri)
if err != nil {
return nil, trace.Wrap(err)
}
// leaf cluster cannot have own leaves
if cluster.URI.GetLeafClusterName() != "" {
return nil, nil
}
leaves, err := cluster.GetLeafClusters(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
return leaves, nil
}
// AddCluster adds a cluster
func (s *Service) AddCluster(ctx context.Context, webProxyAddress string) (*clusters.Cluster, error) {
cluster, err := s.cfg.Storage.Add(ctx, webProxyAddress)
if err != nil {
return nil, trace.Wrap(err)
}
return cluster, nil
}
// RemoveCluster removes cluster
func (s *Service) RemoveCluster(ctx context.Context, uri string) error {
cluster, err := s.ResolveCluster(uri)
if err != nil {
return trace.Wrap(err)
}
if cluster.Connected() {
if err := cluster.Logout(ctx); err != nil {
return trace.Wrap(err)
}
}
if err := s.cfg.Storage.Remove(ctx, cluster.ProfileName); err != nil {
return trace.Wrap(err)
}
return nil
}
// ResolveCluster resolves a cluster by URI by reading data stored on disk in the profile.
//
// It doesn't make network requests so the returned clusters.Cluster will not include full
// information returned from the web/auth servers.
func (s *Service) ResolveCluster(uri string) (*clusters.Cluster, error) {
cluster, err := s.cfg.Storage.GetByResourceURI(uri)
if err != nil {
return nil, trace.Wrap(err)
}
return cluster, nil
}
// ResolveClusterWithDetails returns fully detailed cluster information. It makes requests to the auth server and includes
// details about the cluster and logged in user.
func (s *Service) ResolveClusterWithDetails(ctx context.Context, uri string) (*clusters.ClusterWithDetails, error) {
cluster, err := s.ResolveCluster(uri)
if err != nil {
return nil, trace.Wrap(err)
}
withDetails, err := cluster.GetWithDetails(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
return withDetails, nil
}
// ClusterLogout logs a user out from the cluster
func (s *Service) ClusterLogout(ctx context.Context, uri string) error {
cluster, err := s.ResolveCluster(uri)
if err != nil {
return trace.Wrap(err)
}
if err := cluster.Logout(ctx); err != nil {
return trace.Wrap(err)
}
return nil
}
// CreateGateway creates a gateway to given targetURI
func (s *Service) CreateGateway(ctx context.Context, params CreateGatewayParams) (*gateway.Gateway, error) {
s.mu.Lock()
defer s.mu.Unlock()
gateway, err := s.createGateway(ctx, params)
if err != nil {
return nil, trace.Wrap(err)
}
return gateway, nil
}
type GatewayCreator interface {
CreateGateway(context.Context, clusters.CreateGatewayParams) (*gateway.Gateway, error)
}
// createGateway assumes that mu is already held by a public method.
func (s *Service) createGateway(ctx context.Context, params CreateGatewayParams) (*gateway.Gateway, error) {
cliCommandProvider := clusters.NewDbcmdCLICommandProvider(s.cfg.Storage, dbcmd.SystemExecer{})
clusterCreateGatewayParams := clusters.CreateGatewayParams{
TargetURI: params.TargetURI,
TargetUser: params.TargetUser,
TargetSubresourceName: params.TargetSubresourceName,
LocalPort: params.LocalPort,
CLICommandProvider: cliCommandProvider,
TCPPortAllocator: s.cfg.TCPPortAllocator,
OnExpiredCert: s.onExpiredGatewayCert,
}
gateway, err := s.cfg.GatewayCreator.CreateGateway(ctx, clusterCreateGatewayParams)
if err != nil {
return nil, trace.Wrap(err)
}
go func() {
if err := gateway.Serve(); err != nil {
gateway.Log().WithError(err).Warn("Failed to handle a gateway connection.")
}
}()
s.gateways[gateway.URI().String()] = gateway
return gateway, nil
}
func (s *Service) onExpiredGatewayCert(ctx context.Context, gateway *gateway.Gateway) error {
cluster, err := s.ResolveCluster(gateway.TargetURI())
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(s.cfg.GatewayCertReissuer.ReissueCert(ctx, gateway, cluster))
}
// RemoveGateway removes cluster gateway
func (s *Service) RemoveGateway(gatewayURI string) error {
s.mu.Lock()
defer s.mu.Unlock()
gateway, err := s.findGateway(gatewayURI)
if err != nil {
return trace.Wrap(err)
}
if err := s.removeGateway(gateway); err != nil {
return trace.Wrap(err)
}
return nil
}
// removeGateway assumes that mu is already held by a public method.
func (s *Service) removeGateway(gateway *gateway.Gateway) error {
// If gateway.Close() fails it most likely means it was called on a gateway that was already
// closed and that we have a race condition. Let's return an error in that case.
if err := gateway.Close(); err != nil {
return trace.Wrap(err)
}
delete(s.gateways, gateway.URI().String())
return nil
}
// findGateway assumes that mu is already held by a public method.
func (s *Service) findGateway(gatewayURI string) (*gateway.Gateway, error) {
if gateway, ok := s.gateways[gatewayURI]; ok {
return gateway, nil
}
return nil, trace.NotFound("gateway is not found: %v", gatewayURI)
}
// ListGateways lists gateways
func (s *Service) ListGateways() []gateway.Gateway {
s.mu.RLock()
defer s.mu.RUnlock()
gws := make([]gateway.Gateway, 0, len(s.gateways))
for _, gateway := range s.gateways {
gws = append(gws, *gateway)
}
return gws
}
// SetGatewayTargetSubresourceName updates the TargetSubresourceName field of a gateway stored in
// s.gateways.
func (s *Service) SetGatewayTargetSubresourceName(gatewayURI, targetSubresourceName string) (*gateway.Gateway, error) {
s.mu.Lock()
defer s.mu.Unlock()
gateway, err := s.findGateway(gatewayURI)
if err != nil {
return nil, trace.Wrap(err)
}
gateway.SetTargetSubresourceName(targetSubresourceName)
return gateway, nil
}
// SetGatewayLocalPort creates a new gateway with the given port, swaps it with the old gateway
// under the same URI in s.gateways and then closes the old gateway. It doesn't fetch a fresh db
// cert.
//
// If gateway.NewWithLocalPort fails it's imperative that the current gateway is kept intact. This
// way if the user attempts to change the port to one that cannot be obtained, they're able to
// correct that mistake and choose a different port.
//
// SetGatewayLocalPort is a noop if port is equal to the existing port.
func (s *Service) SetGatewayLocalPort(gatewayURI, localPort string) (*gateway.Gateway, error) {
s.mu.Lock()
defer s.mu.Unlock()
oldGateway, err := s.findGateway(gatewayURI)
if err != nil {
return nil, trace.Wrap(err)
}
if localPort == oldGateway.LocalPort() {
return oldGateway, nil
}
newGateway, err := gateway.NewWithLocalPort(oldGateway, localPort)
if err != nil {
return nil, trace.Wrap(err)
}
if err := s.removeGateway(oldGateway); err != nil {
// s.removeGateway() fails only if it was called on a gateway that was already close. This
// shouldn't happen and would mean that we have a race condition.
//
// Rather than continuing in presence of the race condition, let's attempt to close the new
// gateway (since it shouldn't be used anyway) and return the error.
if newGatewayCloseErr := newGateway.Close(); newGatewayCloseErr != nil {
newGateway.Log().Warnf(
"Failed to close the new gateway after failing to close the old gateway: %v",
newGatewayCloseErr,
)
}
return nil, trace.Wrap(err)
}
s.gateways[gatewayURI] = newGateway
go func() {
if err := newGateway.Serve(); err != nil {
newGateway.Log().WithError(err).Warn("Failed to handle a gateway connection.")
}
}()
return newGateway, nil
}
// GetServers accepts parameterized input to enable searching, sorting, and pagination.
func (s *Service) GetServers(ctx context.Context, req *api.GetServersRequest) (*clusters.GetServersResponse, error) {
cluster, err := s.ResolveCluster(req.ClusterUri)
if err != nil {
return nil, trace.Wrap(err)
}
response, err := cluster.GetServers(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}
return response, nil
}
func (s *Service) GetRequestableRoles(ctx context.Context, req *api.GetRequestableRolesRequest) (*api.GetRequestableRolesResponse, error) {
cluster, err := s.ResolveCluster(req.ClusterUri)
if err != nil {
return nil, trace.Wrap(err)
}
response, err := cluster.GetRequestableRoles(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}
return &api.GetRequestableRolesResponse{
Roles: response.RequestableRoles,
ApplicableRoles: response.ApplicableRolesForResources,
}, nil
}
// GetAccessRequests returns all access requests with filtered input
func (s *Service) GetAccessRequests(ctx context.Context, req *api.GetAccessRequestsRequest) ([]clusters.AccessRequest, error) {
cluster, err := s.ResolveCluster(req.ClusterUri)
if err != nil {
return nil, trace.Wrap(err)
}
response, err := cluster.GetAccessRequests(ctx, types.AccessRequestFilter{})
if err != nil {
return nil, trace.Wrap(err)
}
return response, nil
}
// GetAccessRequest returns AccessRequests filtered by ID
func (s *Service) GetAccessRequest(ctx context.Context, req *api.GetAccessRequestRequest) (*clusters.AccessRequest, error) {
if req.AccessRequestId == "" {
return nil, trace.BadParameter("missing request id")
}
cluster, err := s.ResolveCluster(req.ClusterUri)
if err != nil {
return nil, trace.Wrap(err)
}
response, err := cluster.GetAccessRequest(ctx, types.AccessRequestFilter{
ID: req.AccessRequestId,
})
if err != nil {
return nil, trace.Wrap(err)
}
return response, nil
}
// CreateAccessRequest creates an access request
func (s *Service) CreateAccessRequest(ctx context.Context, req *api.CreateAccessRequestRequest) (*clusters.AccessRequest, error) {
cluster, err := s.ResolveCluster(req.RootClusterUri)
if err != nil {
return nil, trace.Wrap(err)
}
request, err := cluster.CreateAccessRequest(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}
return request, nil
}
func (s *Service) ReviewAccessRequest(ctx context.Context, req *api.ReviewAccessRequestRequest) (*clusters.AccessRequest, error) {
cluster, err := s.ResolveCluster(req.RootClusterUri)
if err != nil {
return nil, trace.Wrap(err)
}
response, err := cluster.ReviewAccessRequest(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}
return response, nil
}
func (s *Service) DeleteAccessRequest(ctx context.Context, req *api.DeleteAccessRequestRequest) error {
if req.AccessRequestId == "" {
return trace.BadParameter("missing request id")
}
cluster, err := s.ResolveCluster((req.RootClusterUri))
if err != nil {
return trace.Wrap(err)
}
err = cluster.DeleteAccessRequest(ctx, req)
if err != nil {
return trace.Wrap(err)
}
return nil
}
func (s *Service) AssumeRole(ctx context.Context, req *api.AssumeRoleRequest) error {
cluster, err := s.ResolveCluster(req.RootClusterUri)
if err != nil {
return trace.Wrap(err)
}
err = cluster.AssumeRole(ctx, req)
if err != nil {
return trace.Wrap(err)
}
return nil
}
// GetKubes accepts parameterized input to enable searching, sorting, and pagination.
func (s *Service) GetKubes(ctx context.Context, req *api.GetKubesRequest) (*clusters.GetKubesResponse, error) {
cluster, err := s.ResolveCluster(req.ClusterUri)
if err != nil {
return nil, trace.Wrap(err)
}
response, err := cluster.GetKubes(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}
return response, nil
}
func (s *Service) ReportUsageEvent(req *api.ReportUsageEventRequest) error {
prehogEvent, err := usagereporter.GetAnonymizedPrehogEvent(req)
if err != nil {
return trace.Wrap(err)
}
s.usageReporter.AddEventsToQueue(prehogEvent)
return nil
}
// Stop terminates all cluster open connections
func (s *Service) Stop() {
s.mu.RLock()
defer s.mu.RUnlock()
s.cfg.Log.Info("Stopping")
for _, gateway := range s.gateways {
gateway.Close()
}
timeoutCtx, cancel := context.WithTimeout(s.closeContext, time.Second*10)
defer cancel()
if err := s.usageReporter.GracefulStop(timeoutCtx); err != nil {
s.cfg.Log.WithError(err).Error("Gracefully stopping usage reporter failed")
}
// s.closeContext is used for the tshd events client which might make requests as long as any of
// the resources managed by daemon.Service are up and running. So let's cancel the context only
// after closing those resources.
s.cancel()
}
// UpdateAndDialTshdEventsServerAddress allows the Electron app to provide the tshd events server
// address.
//
// The startup of the app is orchestrated so that this method is called before any other method on
// daemon.Service. This way all the other code in daemon.Service can assume that the tshd events
// client is available right from the beginning, without the need for nil checks.
func (s *Service) UpdateAndDialTshdEventsServerAddress(serverAddress string) error {
s.mu.Lock()
defer s.mu.Unlock()
withCreds, err := s.cfg.CreateTshdEventsClientCredsFunc()
if err != nil {
return trace.Wrap(err)
}
conn, err := grpc.Dial(serverAddress, withCreds)
if err != nil {
return trace.Wrap(err)
}
client := api.NewTshdEventsServiceClient(conn)
// If the need arises to reuse the client in other places,
// read https://github.com/gravitational/teleport/pull/17950#discussion_r1039434456
s.cfg.GatewayCertReissuer.TSHDEventsClient = client
return nil
}
func (s *Service) TransferFile(ctx context.Context, request *api.FileTransferRequest, sendProgress clusters.FileTransferProgressSender) error {
cluster, err := s.ResolveCluster(request.GetServerUri())
if err != nil {
return trace.Wrap(err)
}
return cluster.TransferFile(ctx, request, sendProgress)
}
// Service is the daemon service
type Service struct {
cfg *Config
// mu guards gateways and the creation of tshdEventsClient.
mu sync.RWMutex
// closeContext is canceled when Service is getting stopped. It is used as a context for the calls
// to the tshd events gRPC client.
closeContext context.Context
cancel context.CancelFunc
// gateways holds the long-running gateways for resources on different clusters. So far it's been
// used mostly for database gateways but it has potential to be used for app access as well.
gateways map[string]*gateway.Gateway
// usageReporter batches the events and sends them to prehog
usageReporter *usagereporter.UsageReporter
}
type CreateGatewayParams struct {
TargetURI string
TargetUser string
TargetSubresourceName string
LocalPort string
}