From 475753f3266cf0056d4fb23c8ce2d885cd511942 Mon Sep 17 00:00:00 2001 From: Marco Dinis Date: Wed, 18 Dec 2024 09:16:59 +0000 Subject: [PATCH 01/64] AWS OIDC: List Deployed Database Services - implementation (#49331) * AWS OIDC: List Deployed Database Services - implementation This PR implements the List Deployed Database Services. This will be used to let the user know which deployed database services were deployed during the Enroll New Resource / RDS flows. * validate region for dashboard url --- lib/auth/integration/integrationv1/awsoidc.go | 52 +++ .../integration/integrationv1/awsoidc_test.go | 10 + lib/integrations/awsoidc/deployservice.go | 15 +- .../awsoidc/listdeployeddatabaseservice.go | 194 ++++++++++ .../listdeployeddatabaseservice_test.go | 360 ++++++++++++++++++ 5 files changed, 628 insertions(+), 3 deletions(-) create mode 100644 lib/integrations/awsoidc/listdeployeddatabaseservice.go create mode 100644 lib/integrations/awsoidc/listdeployeddatabaseservice_test.go diff --git a/lib/auth/integration/integrationv1/awsoidc.go b/lib/auth/integration/integrationv1/awsoidc.go index dfb1b154f5934..bcdff34276968 100644 --- a/lib/auth/integration/integrationv1/awsoidc.go +++ b/lib/auth/integration/integrationv1/awsoidc.go @@ -495,6 +495,58 @@ func (s *AWSOIDCService) DeployDatabaseService(ctx context.Context, req *integra }, nil } +// ListDeployedDatabaseServices lists Database Services deployed into Amazon ECS. +func (s *AWSOIDCService) ListDeployedDatabaseServices(ctx context.Context, req *integrationpb.ListDeployedDatabaseServicesRequest) (*integrationpb.ListDeployedDatabaseServicesResponse, error) { + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := authCtx.CheckAccessToKind(types.KindIntegration, types.VerbUse); err != nil { + return nil, trace.Wrap(err) + } + + clusterName, err := s.cache.GetClusterName() + if err != nil { + return nil, trace.Wrap(err) + } + + awsClientReq, err := s.awsClientReq(ctx, req.Integration, req.Region) + if err != nil { + return nil, trace.Wrap(err) + } + + listDatabaseServicesClient, err := awsoidc.NewListDeployedDatabaseServicesClient(ctx, awsClientReq) + if err != nil { + return nil, trace.Wrap(err) + } + + listDatabaseServicesResponse, err := awsoidc.ListDeployedDatabaseServices(ctx, listDatabaseServicesClient, awsoidc.ListDeployedDatabaseServicesRequest{ + Integration: req.Integration, + TeleportClusterName: clusterName.GetClusterName(), + Region: req.Region, + NextToken: req.NextToken, + }) + if err != nil { + return nil, trace.Wrap(err) + } + + deployedDatabaseServices := make([]*integrationpb.DeployedDatabaseService, 0, len(listDatabaseServicesResponse.DeployedDatabaseServices)) + for _, deployedService := range listDatabaseServicesResponse.DeployedDatabaseServices { + deployedDatabaseServices = append(deployedDatabaseServices, &integrationpb.DeployedDatabaseService{ + Name: deployedService.Name, + ServiceDashboardUrl: deployedService.ServiceDashboardURL, + ContainerEntryPoint: deployedService.ContainerEntryPoint, + ContainerCommand: deployedService.ContainerCommand, + }) + } + + return &integrationpb.ListDeployedDatabaseServicesResponse{ + DeployedDatabaseServices: deployedDatabaseServices, + NextToken: listDatabaseServicesResponse.NextToken, + }, nil +} + // EnrollEKSClusters enrolls EKS clusters into Teleport by installing teleport-kube-agent chart on the clusters. func (s *AWSOIDCService) EnrollEKSClusters(ctx context.Context, req *integrationpb.EnrollEKSClustersRequest) (*integrationpb.EnrollEKSClustersResponse, error) { authCtx, err := s.authorizer.Authorize(ctx) diff --git a/lib/auth/integration/integrationv1/awsoidc_test.go b/lib/auth/integration/integrationv1/awsoidc_test.go index f6cd0e925a48f..6a2497229ab38 100644 --- a/lib/auth/integration/integrationv1/awsoidc_test.go +++ b/lib/auth/integration/integrationv1/awsoidc_test.go @@ -423,6 +423,16 @@ func TestRBAC(t *testing.T) { return err }, }, + { + name: "ListDeployedDatabaseServices", + fn: func() error { + _, err := awsoidService.ListDeployedDatabaseServices(userCtx, &integrationv1.ListDeployedDatabaseServicesRequest{ + Integration: integrationName, + Region: "my-region", + }) + return err + }, + }, } { t.Run(tt.name, func(t *testing.T) { err := tt.fn() diff --git a/lib/integrations/awsoidc/deployservice.go b/lib/integrations/awsoidc/deployservice.go index b9fbc4b99c458..17bfe3a470954 100644 --- a/lib/integrations/awsoidc/deployservice.go +++ b/lib/integrations/awsoidc/deployservice.go @@ -34,6 +34,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/types" + apiaws "github.com/gravitational/teleport/api/utils/aws" "github.com/gravitational/teleport/api/utils/retryutils" "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" "github.com/gravitational/teleport/lib/utils/teleportassets" @@ -445,16 +446,24 @@ func DeployService(ctx context.Context, clt DeployServiceClient, req DeployServi return nil, trace.Wrap(err) } - serviceDashboardURL := fmt.Sprintf("https://%s.console.aws.amazon.com/ecs/v2/clusters/%s/services/%s", req.Region, aws.ToString(req.ClusterName), aws.ToString(req.ServiceName)) - return &DeployServiceResponse{ ClusterARN: aws.ToString(cluster.ClusterArn), ServiceARN: aws.ToString(service.ServiceArn), TaskDefinitionARN: taskDefinitionARN, - ServiceDashboardURL: serviceDashboardURL, + ServiceDashboardURL: serviceDashboardURL(req.Region, aws.ToString(req.ClusterName), aws.ToString(service.ServiceName)), }, nil } +// serviceDashboardURL builds the ECS Service dashboard URL using the AWS Region, the ECS Cluster and Service Names. +// It returns an empty string if region is not valid. +func serviceDashboardURL(region, clusterName, serviceName string) string { + if err := apiaws.IsValidRegion(region); err != nil { + return "" + } + + return fmt.Sprintf("https://%s.console.aws.amazon.com/ecs/v2/clusters/%s/services/%s", region, clusterName, serviceName) +} + type upsertTaskRequest struct { TaskName string TaskRoleARN string diff --git a/lib/integrations/awsoidc/listdeployeddatabaseservice.go b/lib/integrations/awsoidc/listdeployeddatabaseservice.go new file mode 100644 index 0000000000000..c2894902f78fe --- /dev/null +++ b/lib/integrations/awsoidc/listdeployeddatabaseservice.go @@ -0,0 +1,194 @@ +/* + * 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 awsoidc + +import ( + "context" + "log/slog" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ecs" + ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" +) + +// ListDeployedDatabaseServicesRequest contains the required fields to list the deployed database services in Amazon ECS. +type ListDeployedDatabaseServicesRequest struct { + // Region is the AWS Region. + Region string + // Integration is the AWS OIDC Integration name + Integration string + // TeleportClusterName is the name of the Teleport Cluster. + // Used to uniquely identify the ECS Cluster in Amazon. + TeleportClusterName string + // NextToken is the token to be used to fetch the next page. + // If empty, the first page is fetched. + NextToken string +} + +func (req *ListDeployedDatabaseServicesRequest) checkAndSetDefaults() error { + if req.Region == "" { + return trace.BadParameter("region is required") + } + + if req.Integration == "" { + return trace.BadParameter("integration is required") + } + + if req.TeleportClusterName == "" { + return trace.BadParameter("teleport cluster name is required") + } + + return nil +} + +// ListDeployedDatabaseServicesResponse contains a page of Deployed Database Services. +type ListDeployedDatabaseServicesResponse struct { + // DeployedDatabaseServices contains the page of Deployed Database Services. + DeployedDatabaseServices []DeployedDatabaseService `json:"deployedDatabaseServices"` + + // NextToken is used for pagination. + // If non-empty, it can be used to request the next page. + NextToken string `json:"nextToken"` +} + +// DeployedDatabaseService contains a database service that was deployed to Amazon ECS. +type DeployedDatabaseService struct { + // Name is the ECS Service name. + Name string + // ServiceDashboardURL is the Amazon Web Console URL for this ECS Service. + ServiceDashboardURL string + // ContainerEntryPoint is the entry point for the container 0 that is running in the ECS Task. + ContainerEntryPoint []string + // ContainerCommand is the list of arguments that are passed into the ContainerEntryPoint. + ContainerCommand []string +} + +// ListDeployedDatabaseServicesClient describes the required methods to list AWS VPCs. +type ListDeployedDatabaseServicesClient interface { + // ListServices returns a list of services. + ListServices(ctx context.Context, params *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) + // DescribeServices returns ECS Services details. + DescribeServices(ctx context.Context, params *ecs.DescribeServicesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error) + // DescribeTaskDefinition returns an ECS Task Definition. + DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) +} + +type defaultListDeployedDatabaseServicesClient struct { + *ecs.Client +} + +// NewListDeployedDatabaseServicesClient creates a new ListDeployedDatabaseServicesClient using an AWSClientRequest. +func NewListDeployedDatabaseServicesClient(ctx context.Context, req *AWSClientRequest) (ListDeployedDatabaseServicesClient, error) { + ecsClient, err := newECSClient(ctx, req) + if err != nil { + return nil, trace.Wrap(err) + } + + return &defaultListDeployedDatabaseServicesClient{ + Client: ecsClient, + }, nil +} + +// ListDeployedDatabaseServices calls the following AWS API: +// https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ListServices.html +// https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeServices.html +// https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeTaskDefinition.html +// It returns a list of ECS Services running Teleport Database Service and an optional NextToken that can be used to fetch the next page. +func ListDeployedDatabaseServices(ctx context.Context, clt ListDeployedDatabaseServicesClient, req ListDeployedDatabaseServicesRequest) (*ListDeployedDatabaseServicesResponse, error) { + if err := req.checkAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + clusterName := normalizeECSClusterName(req.TeleportClusterName) + + log := slog.With( + "integration", req.Integration, + "aws_region", req.Region, + "ecs_cluster", clusterName, + ) + + // Do not increase this value because ecs.DescribeServices only allows up to 10 services per API call. + maxServicesPerPage := aws.Int32(10) + listServicesInput := &ecs.ListServicesInput{ + Cluster: &clusterName, + MaxResults: maxServicesPerPage, + LaunchType: ecstypes.LaunchTypeFargate, + } + if req.NextToken != "" { + listServicesInput.NextToken = &req.NextToken + } + + listServicesOutput, err := clt.ListServices(ctx, listServicesInput) + if err != nil { + return nil, trace.Wrap(err) + } + + describeServicesOutput, err := clt.DescribeServices(ctx, &ecs.DescribeServicesInput{ + Services: listServicesOutput.ServiceArns, + Include: []ecstypes.ServiceField{ecstypes.ServiceFieldTags}, + Cluster: &clusterName, + }) + if err != nil { + return nil, trace.Wrap(err) + } + + ownershipTags := tags.DefaultResourceCreationTags(req.TeleportClusterName, req.Integration) + + deployedDatabaseServices := []DeployedDatabaseService{} + for _, ecsService := range describeServicesOutput.Services { + log := log.With("ecs_service", aws.ToString(ecsService.ServiceName)) + if !ownershipTags.MatchesECSTags(ecsService.Tags) { + log.WarnContext(ctx, "Missing ownership tags in ECS Service, skipping") + continue + } + + taskDefinitionOut, err := clt.DescribeTaskDefinition(ctx, &ecs.DescribeTaskDefinitionInput{ + TaskDefinition: ecsService.TaskDefinition, + }) + if err != nil { + return nil, trace.Wrap(err) + } + + if len(taskDefinitionOut.TaskDefinition.ContainerDefinitions) == 0 { + log.WarnContext(ctx, "Task has no containers defined, skipping", + "ecs_task_family", aws.ToString(taskDefinitionOut.TaskDefinition.Family), + "ecs_task_revision", taskDefinitionOut.TaskDefinition.Revision, + ) + continue + } + + entryPoint := taskDefinitionOut.TaskDefinition.ContainerDefinitions[0].EntryPoint + command := taskDefinitionOut.TaskDefinition.ContainerDefinitions[0].Command + + deployedDatabaseServices = append(deployedDatabaseServices, DeployedDatabaseService{ + Name: aws.ToString(ecsService.ServiceName), + ServiceDashboardURL: serviceDashboardURL(req.Region, clusterName, aws.ToString(ecsService.ServiceName)), + ContainerEntryPoint: entryPoint, + ContainerCommand: command, + }) + } + + return &ListDeployedDatabaseServicesResponse{ + DeployedDatabaseServices: deployedDatabaseServices, + NextToken: aws.ToString(listServicesOutput.NextToken), + }, nil +} diff --git a/lib/integrations/awsoidc/listdeployeddatabaseservice_test.go b/lib/integrations/awsoidc/listdeployeddatabaseservice_test.go new file mode 100644 index 0000000000000..67f332d495c2b --- /dev/null +++ b/lib/integrations/awsoidc/listdeployeddatabaseservice_test.go @@ -0,0 +1,360 @@ +/* + * 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 awsoidc + +import ( + "context" + "fmt" + "strconv" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ecs" + ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" + "github.com/google/go-cmp/cmp" + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" +) + +func TestListDeployedDatabaseServicesRequest(t *testing.T) { + isBadParamErrFn := func(tt require.TestingT, err error, i ...any) { + require.True(tt, trace.IsBadParameter(err), "expected bad parameter, got %v", err) + } + + baseReqFn := func() ListDeployedDatabaseServicesRequest { + return ListDeployedDatabaseServicesRequest{ + TeleportClusterName: "mycluster", + Region: "eu-west-2", + Integration: "my-integration", + } + } + + for _, tt := range []struct { + name string + req func() ListDeployedDatabaseServicesRequest + errCheck require.ErrorAssertionFunc + reqWithDefaults ListDeployedDatabaseServicesRequest + }{ + { + name: "no fields", + req: func() ListDeployedDatabaseServicesRequest { + return ListDeployedDatabaseServicesRequest{} + }, + errCheck: isBadParamErrFn, + }, + { + name: "missing teleport cluster name", + req: func() ListDeployedDatabaseServicesRequest { + r := baseReqFn() + r.TeleportClusterName = "" + return r + }, + errCheck: isBadParamErrFn, + }, + { + name: "missing region", + req: func() ListDeployedDatabaseServicesRequest { + r := baseReqFn() + r.Region = "" + return r + }, + errCheck: isBadParamErrFn, + }, + { + name: "missing integration", + req: func() ListDeployedDatabaseServicesRequest { + r := baseReqFn() + r.Integration = "" + return r + }, + errCheck: isBadParamErrFn, + }, + } { + t.Run(tt.name, func(t *testing.T) { + r := tt.req() + err := r.checkAndSetDefaults() + tt.errCheck(t, err) + + if err != nil { + return + } + + require.Empty(t, cmp.Diff(tt.reqWithDefaults, r)) + }) + } +} + +type mockListECSClient struct { + pageSize int + + clusterName string + services []*ecstypes.Service + mapServices map[string]ecstypes.Service + taskDefinition map[string]*ecstypes.TaskDefinition +} + +func (m *mockListECSClient) ListServices(ctx context.Context, params *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) { + ret := &ecs.ListServicesOutput{} + if aws.ToString(params.Cluster) != m.clusterName { + return ret, nil + } + + requestedPage := 1 + + totalEndpoints := len(m.services) + + if params.NextToken != nil { + currentMarker, err := strconv.Atoi(*params.NextToken) + if err != nil { + return nil, trace.Wrap(err) + } + requestedPage = currentMarker + } + + sliceStart := m.pageSize * (requestedPage - 1) + sliceEnd := m.pageSize * requestedPage + if sliceEnd > totalEndpoints { + sliceEnd = totalEndpoints + } + + for _, service := range m.services[sliceStart:sliceEnd] { + ret.ServiceArns = append(ret.ServiceArns, aws.ToString(service.ServiceArn)) + } + + if sliceEnd < totalEndpoints { + nextToken := strconv.Itoa(requestedPage + 1) + ret.NextToken = &nextToken + } + + return ret, nil +} + +func (m *mockListECSClient) DescribeServices(ctx context.Context, params *ecs.DescribeServicesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error) { + ret := &ecs.DescribeServicesOutput{} + if aws.ToString(params.Cluster) != m.clusterName { + return ret, nil + } + + for _, serviceARN := range params.Services { + ret.Services = append(ret.Services, m.mapServices[serviceARN]) + } + return ret, nil +} + +func (m *mockListECSClient) DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) { + ret := &ecs.DescribeTaskDefinitionOutput{} + ret.TaskDefinition = m.taskDefinition[aws.ToString(params.TaskDefinition)] + + return ret, nil +} + +func dummyServiceTask(idx int) (ecstypes.Service, *ecstypes.TaskDefinition) { + taskName := fmt.Sprintf("task-family-name-%d", idx) + serviceARN := fmt.Sprintf("arn:eks:service-%d", idx) + + ecsTask := &ecstypes.TaskDefinition{ + Family: aws.String(taskName), + ContainerDefinitions: []ecstypes.ContainerDefinition{{ + EntryPoint: []string{"teleport"}, + Command: []string{"start"}, + }}, + } + + ecsService := ecstypes.Service{ + ServiceArn: aws.String(serviceARN), + ServiceName: aws.String(fmt.Sprintf("database-service-vpc-%d", idx)), + TaskDefinition: aws.String(taskName), + Tags: []ecstypes.Tag{ + {Key: aws.String("teleport.dev/cluster"), Value: aws.String("my-cluster")}, + {Key: aws.String("teleport.dev/integration"), Value: aws.String("my-integration")}, + {Key: aws.String("teleport.dev/origin"), Value: aws.String("integration_awsoidc")}, + }, + } + + return ecsService, ecsTask +} + +func TestListDeployedDatabaseServices(t *testing.T) { + ctx := context.Background() + + const pageSize = 100 + t.Run("pagination", func(t *testing.T) { + totalServices := 203 + + allServices := make([]*ecstypes.Service, 0, totalServices) + mapServices := make(map[string]ecstypes.Service, totalServices) + allTasks := make(map[string]*ecstypes.TaskDefinition, totalServices) + for i := 0; i < totalServices; i++ { + ecsService, ecsTask := dummyServiceTask(i) + allTasks[aws.ToString(ecsTask.Family)] = ecsTask + mapServices[aws.ToString(ecsService.ServiceArn)] = ecsService + allServices = append(allServices, &ecsService) + } + + mockListClient := &mockListECSClient{ + pageSize: pageSize, + clusterName: "my-cluster-teleport", + mapServices: mapServices, + services: allServices, + taskDefinition: allTasks, + } + + // First page must return pageSize number of Endpoints + resp, err := ListDeployedDatabaseServices(ctx, mockListClient, ListDeployedDatabaseServicesRequest{ + Integration: "my-integration", + TeleportClusterName: "my-cluster", + Region: "us-east-1", + }) + require.NoError(t, err) + require.NotEmpty(t, resp.NextToken) + require.Len(t, resp.DeployedDatabaseServices, pageSize) + require.Equal(t, "database-service-vpc-0", resp.DeployedDatabaseServices[0].Name) + require.Equal(t, "https://us-east-1.console.aws.amazon.com/ecs/v2/clusters/my-cluster-teleport/services/database-service-vpc-0", resp.DeployedDatabaseServices[0].ServiceDashboardURL) + require.Equal(t, []string{"teleport"}, resp.DeployedDatabaseServices[0].ContainerEntryPoint) + require.Equal(t, []string{"start"}, resp.DeployedDatabaseServices[0].ContainerCommand) + + // Second page must return pageSize number of Endpoints + nextPageToken := resp.NextToken + resp, err = ListDeployedDatabaseServices(ctx, mockListClient, ListDeployedDatabaseServicesRequest{ + Integration: "my-integration", + TeleportClusterName: "my-cluster", + Region: "us-east-1", + NextToken: nextPageToken, + }) + require.NoError(t, err) + require.NotEmpty(t, resp.NextToken) + require.Len(t, resp.DeployedDatabaseServices, pageSize) + require.Equal(t, "https://us-east-1.console.aws.amazon.com/ecs/v2/clusters/my-cluster-teleport/services/database-service-vpc-100", resp.DeployedDatabaseServices[0].ServiceDashboardURL) + + // Third page must return only the remaining Endpoints and an empty nextToken + nextPageToken = resp.NextToken + resp, err = ListDeployedDatabaseServices(ctx, mockListClient, ListDeployedDatabaseServicesRequest{ + Integration: "my-integration", + TeleportClusterName: "my-cluster", + Region: "us-east-1", + NextToken: nextPageToken, + }) + require.NoError(t, err) + require.Empty(t, resp.NextToken) + require.Len(t, resp.DeployedDatabaseServices, 3) + }) + + for _, tt := range []struct { + name string + req ListDeployedDatabaseServicesRequest + mockClient func() *mockListECSClient + errCheck require.ErrorAssertionFunc + respCheck func(*testing.T, *ListDeployedDatabaseServicesResponse) + }{ + { + name: "ignores ECS Services without ownership tags", + req: ListDeployedDatabaseServicesRequest{ + Integration: "my-integration", + TeleportClusterName: "my-cluster", + Region: "us-east-1", + }, + mockClient: func() *mockListECSClient { + ret := &mockListECSClient{ + pageSize: 10, + clusterName: "my-cluster-teleport", + } + ecsService, ecsTask := dummyServiceTask(0) + + ecsServiceAnotherIntegration, ecsTaskAnotherIntegration := dummyServiceTask(1) + ecsServiceAnotherIntegration.Tags = []ecstypes.Tag{{Key: aws.String("teleport.dev/integration"), Value: aws.String("another-integration")}} + + ret.taskDefinition = map[string]*ecstypes.TaskDefinition{ + aws.ToString(ecsTask.Family): ecsTask, + aws.ToString(ecsTaskAnotherIntegration.Family): ecsTaskAnotherIntegration, + } + ret.mapServices = map[string]ecstypes.Service{ + aws.ToString(ecsService.ServiceArn): ecsService, + aws.ToString(ecsServiceAnotherIntegration.ServiceArn): ecsServiceAnotherIntegration, + } + ret.services = append(ret.services, &ecsService) + ret.services = append(ret.services, &ecsServiceAnotherIntegration) + return ret + }, + respCheck: func(t *testing.T, resp *ListDeployedDatabaseServicesResponse) { + require.Len(t, resp.DeployedDatabaseServices, 1, "expected 1 service, got %d", len(resp.DeployedDatabaseServices)) + require.Empty(t, resp.NextToken, "expected an empty NextToken") + + expectedService := DeployedDatabaseService{ + Name: "database-service-vpc-0", + ServiceDashboardURL: "https://us-east-1.console.aws.amazon.com/ecs/v2/clusters/my-cluster-teleport/services/database-service-vpc-0", + ContainerEntryPoint: []string{"teleport"}, + ContainerCommand: []string{"start"}, + } + require.Empty(t, cmp.Diff(expectedService, resp.DeployedDatabaseServices[0])) + }, + errCheck: require.NoError, + }, + { + name: "ignores ECS Services without containers", + req: ListDeployedDatabaseServicesRequest{ + Integration: "my-integration", + TeleportClusterName: "my-cluster", + Region: "us-east-1", + }, + mockClient: func() *mockListECSClient { + ret := &mockListECSClient{ + pageSize: 10, + clusterName: "my-cluster-teleport", + } + ecsService, ecsTask := dummyServiceTask(0) + + ecsServiceWithoutContainers, ecsTaskWithoutContainers := dummyServiceTask(1) + ecsTaskWithoutContainers.ContainerDefinitions = []ecstypes.ContainerDefinition{} + + ret.taskDefinition = map[string]*ecstypes.TaskDefinition{ + aws.ToString(ecsTask.Family): ecsTask, + aws.ToString(ecsTaskWithoutContainers.Family): ecsTaskWithoutContainers, + } + ret.mapServices = map[string]ecstypes.Service{ + aws.ToString(ecsService.ServiceArn): ecsService, + aws.ToString(ecsServiceWithoutContainers.ServiceArn): ecsServiceWithoutContainers, + } + ret.services = append(ret.services, &ecsService) + ret.services = append(ret.services, &ecsServiceWithoutContainers) + return ret + }, + respCheck: func(t *testing.T, resp *ListDeployedDatabaseServicesResponse) { + require.Len(t, resp.DeployedDatabaseServices, 1, "expected 1 service, got %d", len(resp.DeployedDatabaseServices)) + require.Empty(t, resp.NextToken, "expected an empty NextToken") + + expectedService := DeployedDatabaseService{ + Name: "database-service-vpc-0", + ServiceDashboardURL: "https://us-east-1.console.aws.amazon.com/ecs/v2/clusters/my-cluster-teleport/services/database-service-vpc-0", + ContainerEntryPoint: []string{"teleport"}, + ContainerCommand: []string{"start"}, + } + require.Empty(t, cmp.Diff(expectedService, resp.DeployedDatabaseServices[0])) + }, + errCheck: require.NoError, + }, + } { + t.Run(tt.name, func(t *testing.T) { + resp, err := ListDeployedDatabaseServices(ctx, tt.mockClient(), tt.req) + tt.errCheck(t, err) + if tt.respCheck != nil { + tt.respCheck(t, resp) + } + }) + } +} From 3d6d587d3d17b81bf0b95497abe1c65dacf2455d Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Wed, 18 Dec 2024 11:30:18 +0000 Subject: [PATCH 02/64] Workload Identity: Add minimally viable implementation of IssueWorkloadIdentity RPC (#49943) * Add MVP implementation of IssueWorkloadIdentity endpoint * Add experiment flag * Fix TTL shadowing * Support some non-string attributes for rules and templatin * Adjust regex with @timothyb89 's suggestions * Add note on deny rules --- lib/auth/grpcserver.go | 17 + .../experiment/experiment.go | 41 ++ .../workloadidentityv1/issuer_service.go | 608 ++++++++++++++++++ .../workloadidentityv1/issuer_service_test.go | 223 +++++++ .../workloadidentityv1_test.go | 398 ++++++++++++ lib/jwt/jwt.go | 12 + 6 files changed, 1299 insertions(+) create mode 100644 lib/auth/machineid/workloadidentityv1/experiment/experiment.go create mode 100644 lib/auth/machineid/workloadidentityv1/issuer_service.go create mode 100644 lib/auth/machineid/workloadidentityv1/issuer_service_test.go diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go index 2ba196f37fd39..c7c8900f5c82a 100644 --- a/lib/auth/grpcserver.go +++ b/lib/auth/grpcserver.go @@ -5186,6 +5186,23 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) { } workloadidentityv1pb.RegisterWorkloadIdentityResourceServiceServer(server, workloadIdentityResourceService) + clusterName, err := cfg.AuthServer.GetClusterName() + if err != nil { + return nil, trace.Wrap(err, "getting cluster name") + } + workloadIdentityIssuanceService, err := workloadidentityv1.NewIssuanceService(&workloadidentityv1.IssuanceServiceConfig{ + Authorizer: cfg.Authorizer, + Cache: cfg.AuthServer.Cache, + Emitter: cfg.Emitter, + Clock: cfg.AuthServer.GetClock(), + KeyStore: cfg.AuthServer.keyStore, + ClusterName: clusterName.GetClusterName(), + }) + if err != nil { + return nil, trace.Wrap(err, "creating workload identity issuance service") + } + workloadidentityv1pb.RegisterWorkloadIdentityIssuanceServiceServer(server, workloadIdentityIssuanceService) + dbObjectImportRuleService, err := dbobjectimportrulev1.NewDatabaseObjectImportRuleService(dbobjectimportrulev1.DatabaseObjectImportRuleServiceConfig{ Authorizer: cfg.Authorizer, Backend: cfg.AuthServer.Services, diff --git a/lib/auth/machineid/workloadidentityv1/experiment/experiment.go b/lib/auth/machineid/workloadidentityv1/experiment/experiment.go new file mode 100644 index 0000000000000..fafe51ea83d1f --- /dev/null +++ b/lib/auth/machineid/workloadidentityv1/experiment/experiment.go @@ -0,0 +1,41 @@ +// 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 experiment + +import ( + "os" + "sync" +) + +var mu sync.Mutex + +var experimentEnabled = os.Getenv("TELEPORT_WORKLOAD_IDENTITY_UX_EXPERIMENT") == "1" + +// Enabled returns true if the workload identity UX experiment is +// enabled. +func Enabled() bool { + mu.Lock() + defer mu.Unlock() + return experimentEnabled +} + +// SetEnabled sets the experiment enabled flag. +func SetEnabled(enabled bool) { + mu.Lock() + defer mu.Unlock() + experimentEnabled = enabled +} diff --git a/lib/auth/machineid/workloadidentityv1/issuer_service.go b/lib/auth/machineid/workloadidentityv1/issuer_service.go new file mode 100644 index 0000000000000..70a7fa1197974 --- /dev/null +++ b/lib/auth/machineid/workloadidentityv1/issuer_service.go @@ -0,0 +1,608 @@ +// 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 workloadidentityv1 + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/x509" + "log/slog" + "math/big" + "net/url" + "regexp" + "slices" + "strings" + "time" + + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "go.opentelemetry.io/otel" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/gravitational/teleport" + workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" + "github.com/gravitational/teleport/api/observability/tracing" + "github.com/gravitational/teleport/api/types" + apievents "github.com/gravitational/teleport/api/types/events" + "github.com/gravitational/teleport/lib/auth/machineid/workloadidentityv1/experiment" + "github.com/gravitational/teleport/lib/authz" + "github.com/gravitational/teleport/lib/events" + "github.com/gravitational/teleport/lib/jwt" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/tlsca" + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/oidc" +) + +var tracer = otel.Tracer("github.com/gravitational/teleport/lib/auth/machineid/workloadidentityv1") + +// KeyStorer is an interface that provides methods to retrieve keys and +// certificates from the backend. +type KeyStorer interface { + GetTLSCertAndSigner(ctx context.Context, ca types.CertAuthority) ([]byte, crypto.Signer, error) + GetJWTSigner(ctx context.Context, ca types.CertAuthority) (crypto.Signer, error) +} + +type issuerCache interface { + workloadIdentityReader + GetProxies() ([]types.Server, error) + GetCertAuthority(ctx context.Context, id types.CertAuthID, loadKeys bool) (types.CertAuthority, error) +} + +// IssuanceServiceConfig holds configuration options for the IssuanceService. +type IssuanceServiceConfig struct { + Authorizer authz.Authorizer + Cache issuerCache + Clock clockwork.Clock + Emitter apievents.Emitter + Logger *slog.Logger + KeyStore KeyStorer + + ClusterName string +} + +// IssuanceService is the gRPC service for managing workload identity resources. +// It implements the workloadidentityv1pb.WorkloadIdentityIssuanceServiceServer. +type IssuanceService struct { + workloadidentityv1pb.UnimplementedWorkloadIdentityIssuanceServiceServer + + authorizer authz.Authorizer + cache issuerCache + clock clockwork.Clock + emitter apievents.Emitter + logger *slog.Logger + keyStore KeyStorer + + clusterName string +} + +// NewIssuanceService returns a new instance of the IssuanceService. +func NewIssuanceService(cfg *IssuanceServiceConfig) (*IssuanceService, error) { + switch { + case cfg.Cache == nil: + return nil, trace.BadParameter("cache service is required") + case cfg.Authorizer == nil: + return nil, trace.BadParameter("authorizer is required") + case cfg.Emitter == nil: + return nil, trace.BadParameter("emitter is required") + case cfg.KeyStore == nil: + return nil, trace.BadParameter("key store is required") + case cfg.ClusterName == "": + return nil, trace.BadParameter("cluster name is required") + } + + if cfg.Logger == nil { + cfg.Logger = slog.With(teleport.ComponentKey, "workload_identity_issuance.service") + } + if cfg.Clock == nil { + cfg.Clock = clockwork.NewRealClock() + } + return &IssuanceService{ + authorizer: cfg.Authorizer, + cache: cfg.Cache, + clock: cfg.Clock, + emitter: cfg.Emitter, + logger: cfg.Logger, + keyStore: cfg.KeyStore, + clusterName: cfg.ClusterName, + }, nil +} + +// getFieldStringValue returns a string value from the given attribute set. +// The attribute is specified as a dot-separated path to the field in the +// attribute set. +// +// The specified attribute must be a string field. If the attribute is not +// found, an error is returned. +// +// TODO(noah): This function will be replaced by the Teleport predicate language +// in a coming PR. +func getFieldStringValue(attrs *workloadidentityv1pb.Attrs, attr string) (string, error) { + attrParts := strings.Split(attr, ".") + message := attrs.ProtoReflect() + // TODO(noah): Improve errors by including the fully qualified attribute + // (e.g add up the parts of the attribute path processed thus far) + for i, part := range attrParts { + fieldDesc := message.Descriptor().Fields().ByTextName(part) + if fieldDesc == nil { + return "", trace.NotFound("attribute %q not found", part) + } + // We expect the final key to point to a string field - otherwise - we + // return an error. + if i == len(attrParts)-1 { + if !slices.Contains([]protoreflect.Kind{ + protoreflect.StringKind, + protoreflect.BoolKind, + protoreflect.Int32Kind, + protoreflect.Int64Kind, + protoreflect.Uint64Kind, + protoreflect.Uint32Kind, + }, fieldDesc.Kind()) { + return "", trace.BadParameter("attribute %q of type %q cannot be converted to string", part, fieldDesc.Kind()) + } + return message.Get(fieldDesc).String(), nil + } + // If we're not processing the final key part, we expect this to point + // to a message that we can further explore. + if fieldDesc.Kind() != protoreflect.MessageKind { + return "", trace.BadParameter("attribute %q is not a message", part) + } + message = message.Get(fieldDesc).Message() + } + return "", nil +} + +// templateString takes a given input string and replaces any values within +// {{ }} with values from the attribute set. +// +// If the specified value is not found in the attribute set, an error is +// returned. +// +// TODO(noah): In a coming PR, this will be replaced by evaluating the values +// within the handlebars as expressions. +func templateString(in string, attrs *workloadidentityv1pb.Attrs) (string, error) { + re := regexp.MustCompile(`\{\{([^{}]+?)\}\}`) + matches := re.FindAllStringSubmatch(in, -1) + + for _, match := range matches { + attrKey := strings.TrimSpace(match[1]) + value, err := getFieldStringValue(attrs, attrKey) + if err != nil { + return "", trace.Wrap(err, "fetching attribute value for %q", attrKey) + } + // We want to have an implicit rule here that if an attribute is + // included in the template, but is not set, we should refuse to issue + // the credential. + if value == "" { + return "", trace.NotFound("attribute %q unset", attrKey) + } + in = strings.Replace(in, match[0], value, 1) + } + + return in, nil +} + +func evaluateRules( + wi *workloadidentityv1pb.WorkloadIdentity, + attrs *workloadidentityv1pb.Attrs, +) error { + if len(wi.GetSpec().GetRules().GetAllow()) == 0 { + return nil + } +ruleLoop: + for _, rule := range wi.GetSpec().GetRules().GetAllow() { + for _, condition := range rule.GetConditions() { + val, err := getFieldStringValue(attrs, condition.Attribute) + if err != nil { + return trace.Wrap(err) + } + if val != condition.Equals { + continue ruleLoop + } + } + return nil + } + // TODO: Eventually, we'll need to work support for deny rules into here. + return trace.AccessDenied("no matching rule found") +} + +func (s *IssuanceService) deriveAttrs( + authzCtx *authz.Context, + workloadAttrs *workloadidentityv1pb.WorkloadAttrs, +) (*workloadidentityv1pb.Attrs, error) { + attrs := &workloadidentityv1pb.Attrs{ + Workload: workloadAttrs, + User: &workloadidentityv1pb.UserAttrs{ + Name: authzCtx.Identity.GetIdentity().Username, + IsBot: authzCtx.Identity.GetIdentity().BotName != "", + BotName: authzCtx.Identity.GetIdentity().BotName, + Labels: authzCtx.User.GetAllLabels(), + }, + } + + return attrs, nil +} + +var defaultMaxTTL = 24 * time.Hour + +func (s *IssuanceService) IssueWorkloadIdentity( + ctx context.Context, + req *workloadidentityv1pb.IssueWorkloadIdentityRequest, +) (*workloadidentityv1pb.IssueWorkloadIdentityResponse, error) { + if !experiment.Enabled() { + return nil, trace.AccessDenied("workload identity issuance experiment is disabled") + } + + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + switch { + case req.GetName() == "": + return nil, trace.BadParameter("name: is required") + case req.GetCredential() == nil: + return nil, trace.BadParameter("at least one credential type must be requested") + } + + wi, err := s.cache.GetWorkloadIdentity(ctx, req.GetName()) + if err != nil { + return nil, trace.Wrap(err) + } + // Check the principal has access to the workload identity resource by + // virtue of WorkloadIdentityLabels on a role. + if err := authCtx.Checker.CheckAccess( + types.Resource153ToResourceWithLabels(wi), + services.AccessState{}, + ); err != nil { + return nil, trace.Wrap(err) + } + + attrs, err := s.deriveAttrs(authCtx, req.GetWorkloadAttrs()) + if err != nil { + return nil, trace.Wrap(err, "deriving attributes") + } + // Evaluate any rules explicitly configured by the user + if err := evaluateRules(wi, attrs); err != nil { + return nil, trace.Wrap(err) + } + + // Perform any templating + spiffeIDPath, err := templateString(wi.GetSpec().GetSpiffe().GetId(), attrs) + if err != nil { + return nil, trace.Wrap(err, "templating spec.spiffe.id") + } + spiffeID, err := spiffeid.FromURI(&url.URL{ + Scheme: "spiffe", + Host: s.clusterName, + Path: spiffeIDPath, + }) + if err != nil { + return nil, trace.Wrap(err, "creating SPIFFE ID") + } + + hint, err := templateString(wi.GetSpec().GetSpiffe().GetHint(), attrs) + if err != nil { + return nil, trace.Wrap(err, "templating spec.spiffe.hint") + } + + // TODO(noah): Add more sophisticated control of the TTL. + ttl := time.Hour + if req.RequestedTtl != nil && req.RequestedTtl.AsDuration() != 0 { + ttl = req.RequestedTtl.AsDuration() + if ttl > defaultMaxTTL { + ttl = defaultMaxTTL + } + } + + now := s.clock.Now() + notBefore := now.Add(-1 * time.Minute) + notAfter := now.Add(ttl) + + // Prepare event + evt := &apievents.SPIFFESVIDIssued{ + Metadata: apievents.Metadata{ + Type: events.SPIFFESVIDIssuedEvent, + Code: events.SPIFFESVIDIssuedSuccessCode, + }, + UserMetadata: authz.ClientUserMetadata(ctx), + ConnectionMetadata: authz.ConnectionMetadata(ctx), + SPIFFEID: spiffeID.String(), + Hint: hint, + WorkloadIdentity: wi.GetMetadata().GetName(), + WorkloadIdentityRevision: wi.GetMetadata().GetRevision(), + } + cred := &workloadidentityv1pb.Credential{ + WorkloadIdentityName: wi.GetMetadata().GetName(), + WorkloadIdentityRevision: wi.GetMetadata().GetRevision(), + + SpiffeId: spiffeID.String(), + Hint: hint, + + ExpiresAt: timestamppb.New(notAfter), + Ttl: durationpb.New(ttl), + } + + switch v := req.GetCredential().(type) { + case *workloadidentityv1pb.IssueWorkloadIdentityRequest_X509SvidParams: + evt.SVIDType = "x509" + certDer, certSerial, err := s.issueX509SVID( + ctx, + v.X509SvidParams, + notBefore, + notAfter, + spiffeID, + ) + if err != nil { + return nil, trace.Wrap(err, "issuing X509 SVID") + } + serialStr := serialString(certSerial) + cred.Credential = &workloadidentityv1pb.Credential_X509Svid{ + X509Svid: &workloadidentityv1pb.X509SVIDCredential{ + Cert: certDer, + SerialNumber: serialStr, + }, + } + evt.SerialNumber = serialStr + case *workloadidentityv1pb.IssueWorkloadIdentityRequest_JwtSvidParams: + evt.SVIDType = "jwt" + signedJwt, jti, err := s.issueJWTSVID( + ctx, + v.JwtSvidParams, + now, + notAfter, + spiffeID, + ) + if err != nil { + return nil, trace.Wrap(err, "issuing JWT SVID") + } + cred.Credential = &workloadidentityv1pb.Credential_JwtSvid{ + JwtSvid: &workloadidentityv1pb.JWTSVIDCredential{ + Jwt: signedJwt, + Jti: jti, + }, + } + evt.JTI = jti + default: + return nil, trace.BadParameter("credential: unknown type %T", req.GetCredential()) + } + + if err := s.emitter.EmitAuditEvent(ctx, evt); err != nil { + s.logger.WarnContext( + ctx, + "failed to emit audit event for SVID issuance", + "error", err, + "event", evt, + ) + } + + return &workloadidentityv1pb.IssueWorkloadIdentityResponse{ + Credential: cred, + }, nil +} + +func generateCertSerial() (*big.Int, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + return rand.Int(rand.Reader, serialNumberLimit) +} + +func x509Template( + serialNumber *big.Int, + notBefore time.Time, + notAfter time.Time, + spiffeID spiffeid.ID, +) *x509.Certificate { + return &x509.Certificate{ + SerialNumber: serialNumber, + NotBefore: notBefore, + NotAfter: notAfter, + // SPEC(X509-SVID) 4.3. Key Usage: + // - Leaf SVIDs MUST NOT set keyCertSign or cRLSign. + // - Leaf SVIDs MUST set digitalSignature + // - They MAY set keyEncipherment and/or keyAgreement; + KeyUsage: x509.KeyUsageDigitalSignature | + x509.KeyUsageKeyEncipherment | + x509.KeyUsageKeyAgreement, + // SPEC(X509-SVID) 4.4. Extended Key Usage: + // - Leaf SVIDs SHOULD include this extension, and it MAY be marked as critical. + // - When included, fields id-kp-serverAuth and id-kp-clientAuth MUST be set. + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth, + }, + // SPEC(X509-SVID) 4.1. Basic Constraints: + // - leaf certificates MUST set the cA field to false + BasicConstraintsValid: true, + IsCA: false, + + // SPEC(X509-SVID) 2. SPIFFE ID: + // - The corresponding SPIFFE ID is set as a URI type in the Subject Alternative Name extension + // - An X.509 SVID MUST contain exactly one URI SAN, and by extension, exactly one SPIFFE ID. + // - An X.509 SVID MAY contain any number of other SAN field types, including DNS SANs. + URIs: []*url.URL{spiffeID.URL()}, + } +} + +func (s *IssuanceService) getX509CA( + ctx context.Context, +) (_ *tlsca.CertAuthority, err error) { + ctx, span := tracer.Start(ctx, "IssuanceService/getX509CA") + defer func() { tracing.EndSpan(span, err) }() + + ca, err := s.cache.GetCertAuthority(ctx, types.CertAuthID{ + Type: types.SPIFFECA, + DomainName: s.clusterName, + }, true) + tlsCert, tlsSigner, err := s.keyStore.GetTLSCertAndSigner(ctx, ca) + if err != nil { + return nil, trace.Wrap(err, "getting CA cert and key") + } + tlsCA, err := tlsca.FromCertAndSigner(tlsCert, tlsSigner) + if err != nil { + return nil, trace.Wrap(err) + } + return tlsCA, nil +} + +func (s *IssuanceService) issueX509SVID( + ctx context.Context, + params *workloadidentityv1pb.X509SVIDParams, + notBefore time.Time, + notAfter time.Time, + spiffeID spiffeid.ID, +) (_ []byte, _ *big.Int, err error) { + ctx, span := tracer.Start(ctx, "IssuanceService/issueX509SVID") + defer func() { tracing.EndSpan(span, err) }() + + switch { + case params == nil: + return nil, nil, trace.BadParameter("x509_svid_params: is required") + case len(params.PublicKey) == 0: + return nil, nil, trace.BadParameter("x509_svid_params.public_key: is required") + } + + pubKey, err := x509.ParsePKIXPublicKey(params.PublicKey) + if err != nil { + return nil, nil, trace.Wrap(err, "parsing public key") + } + + certSerial, err := generateCertSerial() + if err != nil { + return nil, nil, trace.Wrap(err, "generating certificate serial") + } + template := x509Template(certSerial, notBefore, notAfter, spiffeID) + + ca, err := s.getX509CA(ctx) + if err != nil { + return nil, nil, trace.Wrap(err, "fetching CA to sign X509 SVID") + } + certBytes, err := x509.CreateCertificate( + rand.Reader, template, ca.Cert, pubKey, ca.Signer, + ) + if err != nil { + return nil, nil, trace.Wrap(err) + } + + return certBytes, certSerial, nil +} + +const jtiLength = 16 + +func (s *IssuanceService) getJWTIssuerKey( + ctx context.Context, +) (_ *jwt.Key, err error) { + ctx, span := tracer.Start(ctx, "IssuanceService/getJWTIssuerKey") + defer func() { tracing.EndSpan(span, err) }() + + ca, err := s.cache.GetCertAuthority(ctx, types.CertAuthID{ + Type: types.SPIFFECA, + DomainName: s.clusterName, + }, true) + if err != nil { + return nil, trace.Wrap(err, "getting SPIFFE CA") + } + + jwtSigner, err := s.keyStore.GetJWTSigner(ctx, ca) + if err != nil { + return nil, trace.Wrap(err, "getting JWT signer") + } + + jwtKey, err := services.GetJWTSigner( + jwtSigner, s.clusterName, s.clock, + ) + if err != nil { + return nil, trace.Wrap(err, "creating JWT signer") + } + return jwtKey, nil +} + +func (s *IssuanceService) issueJWTSVID( + ctx context.Context, + params *workloadidentityv1pb.JWTSVIDParams, + now time.Time, + notAfter time.Time, + spiffeID spiffeid.ID, +) (_ string, _ string, err error) { + ctx, span := tracer.Start(ctx, "IssuanceService/issueJWTSVID") + defer func() { tracing.EndSpan(span, err) }() + + switch { + case params == nil: + return "", "", trace.BadParameter("jwt_svid_params: is required") + case len(params.Audiences) == 0: + return "", "", trace.BadParameter("jwt_svid_params.audiences: at least one audience should be specified") + } + + jti, err := utils.CryptoRandomHex(jtiLength) + if err != nil { + return "", "", trace.Wrap(err, "generating JTI") + } + + key, err := s.getJWTIssuerKey(ctx) + if err != nil { + return "", "", trace.Wrap(err, "getting JWT issuer key") + } + + // Determine the public address of the proxy for inclusion in the JWT as + // the issuer for purposes of OIDC compatibility. + issuer, err := oidc.IssuerForCluster(ctx, s.cache, "/workload-identity") + if err != nil { + return "", "", trace.Wrap(err, "determining issuer URI") + } + + signed, err := key.SignJWTSVID(jwt.SignParamsJWTSVID{ + Audiences: params.Audiences, + SPIFFEID: spiffeID, + JTI: jti, + Issuer: issuer, + + SetIssuedAt: now, + SetExpiry: notAfter, + }) + if err != nil { + return "", "", trace.Wrap(err, "signing jwt") + } + + return signed, jti, nil +} + +func (s *IssuanceService) IssueWorkloadIdentities( + ctx context.Context, + req *workloadidentityv1pb.IssueWorkloadIdentitiesRequest, +) (*workloadidentityv1pb.IssueWorkloadIdentitiesResponse, error) { + // TODO(noah): Coming to a PR near you soon! + return nil, trace.NotImplemented("not implemented") +} + +func serialString(serial *big.Int) string { + hex := serial.Text(16) + if len(hex)%2 == 1 { + hex = "0" + hex + } + + out := strings.Builder{} + for i := 0; i < len(hex); i += 2 { + if i != 0 { + out.WriteString(":") + } + out.WriteString(hex[i : i+2]) + } + return out.String() +} diff --git a/lib/auth/machineid/workloadidentityv1/issuer_service_test.go b/lib/auth/machineid/workloadidentityv1/issuer_service_test.go new file mode 100644 index 0000000000000..bf18594416609 --- /dev/null +++ b/lib/auth/machineid/workloadidentityv1/issuer_service_test.go @@ -0,0 +1,223 @@ +// 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 workloadidentityv1 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" + "github.com/gravitational/teleport/api/types" +) + +func Test_getFieldStringValue(t *testing.T) { + tests := []struct { + name string + in *workloadidentityv1pb.Attrs + attr string + want string + requireErr require.ErrorAssertionFunc + }{ + { + name: "success", + in: &workloadidentityv1pb.Attrs{ + User: &workloadidentityv1pb.UserAttrs{ + Name: "jeff", + }, + }, + attr: "user.name", + want: "jeff", + requireErr: require.NoError, + }, + { + name: "bool", + in: &workloadidentityv1pb.Attrs{ + User: &workloadidentityv1pb.UserAttrs{ + Name: "jeff", + }, + Workload: &workloadidentityv1pb.WorkloadAttrs{ + Unix: &workloadidentityv1pb.WorkloadAttrsUnix{ + Attested: true, + }, + }, + }, + attr: "workload.unix.attested", + want: "true", + requireErr: require.NoError, + }, + { + name: "int32", + in: &workloadidentityv1pb.Attrs{ + User: &workloadidentityv1pb.UserAttrs{ + Name: "jeff", + }, + Workload: &workloadidentityv1pb.WorkloadAttrs{ + Unix: &workloadidentityv1pb.WorkloadAttrsUnix{ + Pid: 123, + }, + }, + }, + attr: "workload.unix.pid", + want: "123", + requireErr: require.NoError, + }, + { + name: "uint32", + in: &workloadidentityv1pb.Attrs{ + User: &workloadidentityv1pb.UserAttrs{ + Name: "jeff", + }, + Workload: &workloadidentityv1pb.WorkloadAttrs{ + Unix: &workloadidentityv1pb.WorkloadAttrsUnix{ + Gid: 123, + }, + }, + }, + attr: "workload.unix.gid", + want: "123", + requireErr: require.NoError, + }, + { + name: "non-string final field", + in: &workloadidentityv1pb.Attrs{ + User: &workloadidentityv1pb.UserAttrs{ + Name: "user", + }, + }, + attr: "user", + requireErr: func(t require.TestingT, err error, i ...interface{}) { + require.ErrorContains(t, err, "attribute \"user\" of type \"message\" cannot be converted to string") + }, + }, + { + // We mostly just want this to not panic. + name: "nil root", + in: nil, + attr: "user.name", + want: "", + requireErr: require.NoError, + }, + { + // We mostly just want this to not panic. + name: "nil submessage", + in: &workloadidentityv1pb.Attrs{ + User: nil, + }, + attr: "user.name", + want: "", + requireErr: require.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := getFieldStringValue(tt.in, tt.attr) + tt.requireErr(t, gotErr) + require.Equal(t, tt.want, got) + }) + } +} + +func Test_templateString(t *testing.T) { + tests := []struct { + name string + in string + want string + attrs *workloadidentityv1pb.Attrs + requireErr require.ErrorAssertionFunc + }{ + { + name: "success mixed", + in: "hello{{user.name}}.{{user.name}} {{ workload.kubernetes.pod_name }}//{{ workload.kubernetes.namespace}}", + want: "hellojeff.jeff pod1//default", + attrs: &workloadidentityv1pb.Attrs{ + User: &workloadidentityv1pb.UserAttrs{ + Name: "jeff", + }, + Workload: &workloadidentityv1pb.WorkloadAttrs{ + Kubernetes: &workloadidentityv1pb.WorkloadAttrsKubernetes{ + PodName: "pod1", + Namespace: "default", + }, + }, + }, + requireErr: require.NoError, + }, + { + name: "success with spaces", + in: "hello {{user.name}}", + want: "hello jeff", + attrs: &workloadidentityv1pb.Attrs{ + User: &workloadidentityv1pb.UserAttrs{ + Name: "jeff", + }, + }, + requireErr: require.NoError, + }, + { + name: "fail due to unset", + in: "hello {{workload.kubernetes.pod_name}}", + attrs: &workloadidentityv1pb.Attrs{ + User: &workloadidentityv1pb.UserAttrs{ + Name: "jeff", + }, + }, + requireErr: func(t require.TestingT, err error, i ...interface{}) { + require.ErrorContains(t, err, "attribute \"workload.kubernetes.pod_name\" unset") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := templateString(tt.in, tt.attrs) + tt.requireErr(t, gotErr) + require.Equal(t, tt.want, got) + }) + } +} + +func Test_evaluateRules(t *testing.T) { + attrs := &workloadidentityv1pb.Attrs{ + User: &workloadidentityv1pb.UserAttrs{ + Name: "foo", + }, + } + wi := &workloadidentityv1pb.WorkloadIdentity{ + Kind: types.KindWorkloadIdentity, + Version: types.V1, + Metadata: &headerv1.Metadata{ + Name: "test", + }, + Spec: &workloadidentityv1pb.WorkloadIdentitySpec{ + Rules: &workloadidentityv1pb.WorkloadIdentityRules{ + Allow: []*workloadidentityv1pb.WorkloadIdentityRule{ + { + Conditions: []*workloadidentityv1pb.WorkloadIdentityCondition{ + { + Attribute: "user.name", + Equals: "foo", + }, + }, + }, + }, + }, + }, + } + err := evaluateRules(wi, attrs) + require.NoError(t, err) +} diff --git a/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go b/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go index 1c0601a34dd54..3b7a7b1d85759 100644 --- a/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go +++ b/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go @@ -18,6 +18,7 @@ package workloadidentityv1_test import ( "context" + "crypto/x509" "errors" "fmt" "net" @@ -26,6 +27,7 @@ import ( "testing" "time" + "github.com/go-jose/go-jose/v3/jwt" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/gravitational/trace" @@ -33,6 +35,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/durationpb" headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" @@ -40,9 +43,13 @@ import ( "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/auth/authclient" + "github.com/gravitational/teleport/lib/auth/machineid/workloadidentityv1/experiment" + "github.com/gravitational/teleport/lib/cryptosuites" libevents "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/events/eventstest" + libjwt "github.com/gravitational/teleport/lib/jwt" "github.com/gravitational/teleport/lib/modules" + "github.com/gravitational/teleport/lib/services" ) func TestMain(m *testing.M) { @@ -74,6 +81,397 @@ func newTestTLSServer(t testing.TB) (*auth.TestTLSServer, *eventstest.MockRecord return srv, emitter } +func TestIssueWorkloadIdentity(t *testing.T) { + experimentStatus := experiment.Enabled() + defer experiment.SetEnabled(experimentStatus) + experiment.SetEnabled(true) + + srv, eventRecorder := newTestTLSServer(t) + ctx := context.Background() + clock := srv.Auth().GetClock() + + // Upsert a fake proxy to ensure we have a public address to use for the + // issuer. + proxy, err := types.NewServer("proxy", types.KindProxy, types.ServerSpecV2{ + PublicAddrs: []string{"teleport.example.com"}, + }) + require.NoError(t, err) + err = srv.Auth().UpsertProxy(ctx, proxy) + require.NoError(t, err) + wantIssuer := "https://teleport.example.com/workload-identity" + + // Fetch X509 SPIFFE CA for validation of signature later + spiffeX509CA, err := srv.Auth().GetCertAuthority(ctx, types.CertAuthID{ + Type: types.SPIFFECA, + DomainName: srv.ClusterName(), + }, false) + require.NoError(t, err) + spiffeX509CAPool, err := services.CertPool(spiffeX509CA) + require.NoError(t, err) + // Fetch JWT CA to validate JWTs + jwtCA, err := srv.Auth().GetCertAuthority(ctx, types.CertAuthID{ + Type: types.SPIFFECA, + DomainName: "localhost", + }, true) + require.NoError(t, err) + jwtSigner, err := srv.Auth().GetKeyStore().GetJWTSigner(ctx, jwtCA) + require.NoError(t, err) + kid, err := libjwt.KeyID(jwtSigner.Public()) + require.NoError(t, err) + + wildcardAccess, _, err := auth.CreateUserAndRole( + srv.Auth(), + "dog", + []string{}, + []types.Rule{}, + auth.WithRoleMutator(func(role types.Role) { + role.SetWorkloadIdentityLabels(types.Allow, types.Labels{ + types.Wildcard: []string{types.Wildcard}, + }) + }), + ) + require.NoError(t, err) + wilcardAccessClient, err := srv.NewClient(auth.TestUser(wildcardAccess.GetName())) + require.NoError(t, err) + + specificAccess, _, err := auth.CreateUserAndRole( + srv.Auth(), + "cat", + []string{}, + []types.Rule{}, + auth.WithRoleMutator(func(role types.Role) { + role.SetWorkloadIdentityLabels(types.Allow, types.Labels{ + "foo": []string{"bar"}, + }) + }), + ) + require.NoError(t, err) + specificAccessClient, err := srv.NewClient(auth.TestUser(specificAccess.GetName())) + require.NoError(t, err) + + // Generate a keypair to generate x509 SVIDs for. + workloadKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) + require.NoError(t, err) + workloadKeyPubBytes, err := x509.MarshalPKIXPublicKey(workloadKey.Public()) + require.NoError(t, err) + + // Create some WorkloadIdentity resources + full, err := srv.Auth().CreateWorkloadIdentity(ctx, &workloadidentityv1pb.WorkloadIdentity{ + Kind: types.KindWorkloadIdentity, + Version: types.V1, + Metadata: &headerv1.Metadata{ + Name: "full", + }, + Spec: &workloadidentityv1pb.WorkloadIdentitySpec{ + Rules: &workloadidentityv1pb.WorkloadIdentityRules{ + Allow: []*workloadidentityv1pb.WorkloadIdentityRule{ + { + Conditions: []*workloadidentityv1pb.WorkloadIdentityCondition{ + { + Attribute: "user.name", + Equals: "dog", + }, + { + Attribute: "workload.kubernetes.namespace", + Equals: "default", + }, + }, + }, + }, + }, + Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{ + Id: "/example/{{user.name}}/{{ workload.kubernetes.namespace }}/{{ workload.kubernetes.service_account }}", + Hint: "Wow - what a lovely hint, {{user.name}}!", + }, + }, + }) + require.NoError(t, err) + + workloadAttrs := func(f func(attrs *workloadidentityv1pb.WorkloadAttrs)) *workloadidentityv1pb.WorkloadAttrs { + attrs := &workloadidentityv1pb.WorkloadAttrs{ + Kubernetes: &workloadidentityv1pb.WorkloadAttrsKubernetes{ + Attested: true, + Namespace: "default", + PodName: "test", + ServiceAccount: "bar", + }, + } + if f != nil { + f(attrs) + } + return attrs + } + tests := []struct { + name string + client *authclient.Client + req *workloadidentityv1pb.IssueWorkloadIdentityRequest + requireErr require.ErrorAssertionFunc + assert func(*testing.T, *workloadidentityv1pb.IssueWorkloadIdentityResponse) + }{ + { + name: "jwt svid", + client: wilcardAccessClient, + req: &workloadidentityv1pb.IssueWorkloadIdentityRequest{ + Name: full.GetMetadata().GetName(), + Credential: &workloadidentityv1pb.IssueWorkloadIdentityRequest_JwtSvidParams{ + JwtSvidParams: &workloadidentityv1pb.JWTSVIDParams{ + Audiences: []string{"example.com", "test.example.com"}, + }, + }, + WorkloadAttrs: workloadAttrs(nil), + }, + requireErr: require.NoError, + assert: func(t *testing.T, res *workloadidentityv1pb.IssueWorkloadIdentityResponse) { + cred := res.Credential + require.NotNil(t, res.Credential) + + wantTTL := time.Hour + wantSPIFFEID := "spiffe://localhost/example/dog/default/bar" + require.Empty(t, cmp.Diff( + cred, + &workloadidentityv1pb.Credential{ + Ttl: durationpb.New(wantTTL), + SpiffeId: wantSPIFFEID, + Hint: "Wow - what a lovely hint, dog!", + WorkloadIdentityName: full.GetMetadata().GetName(), + WorkloadIdentityRevision: full.GetMetadata().GetRevision(), + }, + protocmp.Transform(), + protocmp.IgnoreFields( + &workloadidentityv1pb.Credential{}, + "expires_at", + ), + protocmp.IgnoreOneofs( + &workloadidentityv1pb.Credential{}, + "credential", + ), + )) + // Check expiry makes sense + require.WithinDuration(t, clock.Now().Add(wantTTL), cred.GetExpiresAt().AsTime(), time.Second) + + // Check the JWT + parsed, err := jwt.ParseSigned(cred.GetJwtSvid().GetJwt()) + require.NoError(t, err) + + claims := jwt.Claims{} + err = parsed.Claims(jwtSigner.Public(), &claims) + require.NoError(t, err) + // Check headers + require.Len(t, parsed.Headers, 1) + require.Equal(t, kid, parsed.Headers[0].KeyID) + // Check claims + require.Equal(t, wantSPIFFEID, claims.Subject) + require.NotEmpty(t, claims.ID) + require.Equal(t, jwt.Audience{"example.com", "test.example.com"}, claims.Audience) + require.Equal(t, wantIssuer, claims.Issuer) + require.WithinDuration(t, clock.Now().Add(wantTTL), claims.Expiry.Time(), 5*time.Second) + require.WithinDuration(t, clock.Now(), claims.IssuedAt.Time(), 5*time.Second) + + // Check audit log event + evt, ok := eventRecorder.LastEvent().(*events.SPIFFESVIDIssued) + require.True(t, ok) + require.NotEmpty(t, evt.ConnectionMetadata.RemoteAddr) + require.Equal(t, claims.ID, evt.JTI) + require.Equal(t, claims.ID, cred.GetJwtSvid().GetJti()) + require.Empty(t, cmp.Diff( + evt, + &events.SPIFFESVIDIssued{ + Metadata: events.Metadata{ + Type: libevents.SPIFFESVIDIssuedEvent, + Code: libevents.SPIFFESVIDIssuedSuccessCode, + }, + UserMetadata: events.UserMetadata{ + User: wildcardAccess.GetName(), + UserKind: events.UserKind_USER_KIND_HUMAN, + }, + SPIFFEID: "spiffe://localhost/example/dog/default/bar", + SVIDType: "jwt", + Hint: "Wow - what a lovely hint, dog!", + WorkloadIdentity: full.GetMetadata().GetName(), + WorkloadIdentityRevision: full.GetMetadata().GetRevision(), + }, + cmpopts.IgnoreFields( + events.SPIFFESVIDIssued{}, + "ConnectionMetadata", + "JTI", + ), + )) + }, + }, + { + name: "x509 svid", + client: wilcardAccessClient, + req: &workloadidentityv1pb.IssueWorkloadIdentityRequest{ + Name: full.GetMetadata().GetName(), + Credential: &workloadidentityv1pb.IssueWorkloadIdentityRequest_X509SvidParams{ + X509SvidParams: &workloadidentityv1pb.X509SVIDParams{ + PublicKey: workloadKeyPubBytes, + }, + }, + WorkloadAttrs: workloadAttrs(nil), + }, + requireErr: require.NoError, + assert: func(t *testing.T, res *workloadidentityv1pb.IssueWorkloadIdentityResponse) { + cred := res.Credential + require.NotNil(t, res.Credential) + + wantSPIFFEID := "spiffe://localhost/example/dog/default/bar" + wantTTL := time.Hour + require.Empty(t, cmp.Diff( + cred, + &workloadidentityv1pb.Credential{ + Ttl: durationpb.New(wantTTL), + SpiffeId: wantSPIFFEID, + Hint: "Wow - what a lovely hint, dog!", + WorkloadIdentityName: full.GetMetadata().GetName(), + WorkloadIdentityRevision: full.GetMetadata().GetRevision(), + }, + protocmp.Transform(), + protocmp.IgnoreFields( + &workloadidentityv1pb.Credential{}, + "expires_at", + ), + protocmp.IgnoreOneofs( + &workloadidentityv1pb.Credential{}, + "credential", + ), + )) + // Check expiry makes sense + require.WithinDuration(t, clock.Now().Add(wantTTL), cred.GetExpiresAt().AsTime(), time.Second) + + // Check the X509 + cert, err := x509.ParseCertificate(cred.GetX509Svid().GetCert()) + require.NoError(t, err) + // Check included public key matches + require.Equal(t, workloadKey.Public(), cert.PublicKey) + // Check cert expiry + require.WithinDuration(t, clock.Now().Add(wantTTL), cert.NotAfter, time.Second) + // Check cert nbf + require.WithinDuration(t, clock.Now().Add(-1*time.Minute), cert.NotBefore, time.Second) + // Check cert TTL + require.Equal(t, cert.NotAfter.Sub(cert.NotBefore), wantTTL+time.Minute) + + // Check against SPIFFE SPEC + // References are to https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md + // 2: An X.509 SVID MUST contain exactly one URI SAN, and by extension, exactly one SPIFFE ID + require.Len(t, cert.URIs, 1) + require.Equal(t, wantSPIFFEID, cert.URIs[0].String()) + // 4.1: leaf certificates MUST set the cA field to false. + require.False(t, cert.IsCA) + require.Greater(t, cert.KeyUsage&x509.KeyUsageDigitalSignature, 0) + // 4.3: They MAY set keyEncipherment and/or keyAgreement + require.Greater(t, cert.KeyUsage&x509.KeyUsageKeyEncipherment, 0) + require.Greater(t, cert.KeyUsage&x509.KeyUsageKeyAgreement, 0) + // 4.3: Leaf SVIDs MUST NOT set keyCertSign or cRLSign + require.EqualValues(t, 0, cert.KeyUsage&x509.KeyUsageCertSign) + require.EqualValues(t, 0, cert.KeyUsage&x509.KeyUsageCRLSign) + // 4.4: When included, fields id-kp-serverAuth and id-kp-clientAuth MUST be set. + require.Contains(t, cert.ExtKeyUsage, x509.ExtKeyUsageServerAuth) + require.Contains(t, cert.ExtKeyUsage, x509.ExtKeyUsageClientAuth) + + // Check cert signature is valid + _, err = cert.Verify(x509.VerifyOptions{ + Roots: spiffeX509CAPool, + CurrentTime: srv.Auth().GetClock().Now(), + }) + require.NoError(t, err) + + // Check audit log event + evt, ok := eventRecorder.LastEvent().(*events.SPIFFESVIDIssued) + require.True(t, ok) + require.NotEmpty(t, evt.ConnectionMetadata.RemoteAddr) + require.Equal(t, cred.GetX509Svid().GetSerialNumber(), evt.SerialNumber) + require.Empty(t, cmp.Diff( + evt, + &events.SPIFFESVIDIssued{ + Metadata: events.Metadata{ + Type: libevents.SPIFFESVIDIssuedEvent, + Code: libevents.SPIFFESVIDIssuedSuccessCode, + }, + UserMetadata: events.UserMetadata{ + User: wildcardAccess.GetName(), + UserKind: events.UserKind_USER_KIND_HUMAN, + }, + SPIFFEID: "spiffe://localhost/example/dog/default/bar", + SVIDType: "x509", + Hint: "Wow - what a lovely hint, dog!", + WorkloadIdentity: full.GetMetadata().GetName(), + WorkloadIdentityRevision: full.GetMetadata().GetRevision(), + }, + cmpopts.IgnoreFields( + events.SPIFFESVIDIssued{}, + "ConnectionMetadata", + "SerialNumber", + ), + )) + }, + }, + { + name: "unauthorized by rules", + client: wilcardAccessClient, + req: &workloadidentityv1pb.IssueWorkloadIdentityRequest{ + Name: full.GetMetadata().GetName(), + Credential: &workloadidentityv1pb.IssueWorkloadIdentityRequest_JwtSvidParams{ + JwtSvidParams: &workloadidentityv1pb.JWTSVIDParams{ + Audiences: []string{"example.com", "test.example.com"}, + }, + }, + WorkloadAttrs: workloadAttrs(func(attrs *workloadidentityv1pb.WorkloadAttrs) { + attrs.Kubernetes.Namespace = "not-default" + }), + }, + requireErr: func(t require.TestingT, err error, i ...interface{}) { + require.True(t, trace.IsAccessDenied(err)) + }, + }, + { + name: "unauthorized by labels", + client: specificAccessClient, + req: &workloadidentityv1pb.IssueWorkloadIdentityRequest{ + Name: full.GetMetadata().GetName(), + Credential: &workloadidentityv1pb.IssueWorkloadIdentityRequest_JwtSvidParams{ + JwtSvidParams: &workloadidentityv1pb.JWTSVIDParams{ + Audiences: []string{"example.com", "test.example.com"}, + }, + }, + WorkloadAttrs: workloadAttrs(nil), + }, + requireErr: func(t require.TestingT, err error, i ...interface{}) { + require.True(t, trace.IsAccessDenied(err)) + }, + }, + { + name: "does not exist", + client: specificAccessClient, + req: &workloadidentityv1pb.IssueWorkloadIdentityRequest{ + Name: "does-not-exist", + Credential: &workloadidentityv1pb.IssueWorkloadIdentityRequest_JwtSvidParams{ + JwtSvidParams: &workloadidentityv1pb.JWTSVIDParams{ + Audiences: []string{"example.com", "test.example.com"}, + }, + }, + WorkloadAttrs: workloadAttrs(nil), + }, + requireErr: func(t require.TestingT, err error, i ...interface{}) { + require.True(t, trace.IsNotFound(err)) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + eventRecorder.Reset() + c := workloadidentityv1pb.NewWorkloadIdentityIssuanceServiceClient( + tt.client.GetConnection(), + ) + res, err := c.IssueWorkloadIdentity(ctx, tt.req) + tt.requireErr(t, err) + if tt.assert != nil { + tt.assert(t, res) + } + }) + } +} + func TestResourceService_CreateWorkloadIdentity(t *testing.T) { t.Parallel() srv, eventRecorder := newTestTLSServer(t) diff --git a/lib/jwt/jwt.go b/lib/jwt/jwt.go index a0305acf55971..5afe58f20d96a 100644 --- a/lib/jwt/jwt.go +++ b/lib/jwt/jwt.go @@ -279,6 +279,12 @@ type SignParamsJWTSVID struct { // Issuer is the value that should be included in the `iss` claim of the // created token. Issuer string + + // SetExpiry overrides the expiry time of the token. This causes the value + // of TTL to be ignored. + SetExpiry time.Time + // SetIssuedAt overrides the issued at time of the token. + SetIssuedAt time.Time } // SignJWTSVID signs a JWT SVID token. @@ -310,6 +316,12 @@ func (k *Key) SignJWTSVID(p SignParamsJWTSVID) (string, error) { // understand OIDC. Issuer: p.Issuer, } + if !p.SetIssuedAt.IsZero() { + claims.IssuedAt = jwt.NewNumericDate(p.SetIssuedAt) + } + if !p.SetExpiry.IsZero() { + claims.Expiry = jwt.NewNumericDate(p.SetExpiry) + } // > 2.2. Key ID: // >The kid header is optional. From 3db02c362c11af43728927a114625da047c55acb Mon Sep 17 00:00:00 2001 From: rosstimothy <39066650+rosstimothy@users.noreply.github.com> Date: Wed, 18 Dec 2024 08:58:11 -0500 Subject: [PATCH 03/64] Convert lib/joinserver to use slog (#50353) --- lib/joinserver/joinserver.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/joinserver/joinserver.go b/lib/joinserver/joinserver.go index 20933b02a4f99..2f16af283cfc5 100644 --- a/lib/joinserver/joinserver.go +++ b/lib/joinserver/joinserver.go @@ -29,7 +29,6 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "google.golang.org/grpc/peer" "github.com/gravitational/teleport/api/client" @@ -110,7 +109,7 @@ func (s *JoinServiceGRPCServer) RegisterUsingIAMMethod(srv proto.JoinService_Reg if peerInfo, ok := peer.FromContext(srv.Context()); ok { nodeAddr = peerInfo.Addr.String() } - logrus.Warnf("IAM join attempt timed out, node at (%s) is misbehaving or did not close the connection after encountering an error.", nodeAddr) + slog.WarnContext(srv.Context(), "IAM join attempt timed out, agent is misbehaving or did not close the connection after encountering an error", "agent_addr", nodeAddr) // Returning here should cancel any blocked Send or Recv operations. return trace.LimitExceeded("RegisterUsingIAMMethod timed out after %s, terminating the stream on the server", iamJoinRequestTimeout) } @@ -181,7 +180,7 @@ func (s *JoinServiceGRPCServer) RegisterUsingAzureMethod(srv proto.JoinService_R if peerInfo, ok := peer.FromContext(srv.Context()); ok { nodeAddr = peerInfo.Addr.String() } - logrus.Warnf("Azure join attempt timed out, node at (%s) is misbehaving or did not close the connection after encountering an error.", nodeAddr) + slog.WarnContext(srv.Context(), "Azure join attempt timed out, agent is misbehaving or did not close the connection after encountering an error", "agent_addr", nodeAddr) // Returning here should cancel any blocked Send or Recv operations. return trace.LimitExceeded("RegisterUsingAzureMethod timed out after %s, terminating the stream on the server", azureJoinRequestTimeout) } @@ -231,10 +230,10 @@ func setBotParameters(ctx context.Context, req *types.RegisterUsingTokenRequest) if ident.BotInstanceID != "" { // Trust the instance ID from the incoming identity: bots will // attempt to provide it on renewal, assuming it's still valid. - logrus.WithFields(logrus.Fields{ - "bot_name": ident.BotName, - "bot_instance_id": ident.BotInstanceID, - }).Info("bot is rejoining") + slog.InfoContext(ctx, "bot is rejoining", + "bot_name", ident.BotName, + "bot_instance_id", ident.BotInstanceID, + ) req.BotInstanceID = ident.BotInstanceID } else { // Clear any other value from the request: the value must come from a From 3550c4dfc84b5546c9d02435924fdb096c70c139 Mon Sep 17 00:00:00 2001 From: rosstimothy <39066650+rosstimothy@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:09:17 -0500 Subject: [PATCH 04/64] Convert lib/reversetunnel to use slog (#50362) --- lib/reversetunnel/agent.go | 67 ++++++----- lib/reversetunnel/agent_dialer.go | 10 +- lib/reversetunnel/agent_dialer_test.go | 3 +- lib/reversetunnel/agentpool.go | 64 +++++----- lib/reversetunnel/conn.go | 27 +++-- lib/reversetunnel/localsite.go | 102 +++++++++------- lib/reversetunnel/localsite_test.go | 8 +- lib/reversetunnel/peer.go | 13 +-- lib/reversetunnel/rc_manager.go | 18 +-- lib/reversetunnel/remotesite.go | 130 +++++++++++---------- lib/reversetunnel/srv.go | 135 ++++++++++------------ lib/reversetunnel/srv_test.go | 7 +- lib/reversetunnel/transport.go | 70 +++++++---- lib/reversetunnelclient/api_with_roles.go | 4 +- lib/service/service.go | 9 +- 15 files changed, 356 insertions(+), 311 deletions(-) diff --git a/lib/reversetunnel/agent.go b/lib/reversetunnel/agent.go index da41578411067..4bd870c3418d6 100644 --- a/lib/reversetunnel/agent.go +++ b/lib/reversetunnel/agent.go @@ -27,13 +27,13 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "strings" "sync" "time" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport/api/constants" @@ -42,6 +42,7 @@ import ( "github.com/gravitational/teleport/lib/multiplexer" "github.com/gravitational/teleport/lib/reversetunnel/track" "github.com/gravitational/teleport/lib/utils" + logutils "github.com/gravitational/teleport/lib/utils/log" ) type AgentState string @@ -113,8 +114,8 @@ type agentConfig struct { // clock is use to get the current time. Mock clocks can be used for // testing. clock clockwork.Clock - // log is an optional logger. - log logrus.FieldLogger + // logger is an optional logger. + logger *slog.Logger // localAuthAddresses is a list of auth servers to use when dialing back to // the local cluster. localAuthAddresses []string @@ -145,12 +146,13 @@ func (c *agentConfig) checkAndSetDefaults() error { if c.clock == nil { c.clock = clockwork.NewRealClock() } - if c.log == nil { - c.log = logrus.New() + if c.logger == nil { + c.logger = slog.Default() } - c.log = c.log. - WithField("leaseID", c.lease.ID()). - WithField("target", c.addr.String()) + c.logger = c.logger.With( + "lease_id", c.lease.ID(), + "target", c.addr.String(), + ) return nil } @@ -284,7 +286,10 @@ func (a *agent) updateState(state AgentState) (AgentState, error) { prevState := a.state a.state = state - a.log.Debugf("Changing state %s -> %s.", prevState, state) + a.logger.DebugContext(a.ctx, "Agent state updated", + "previous_state", prevState, + "current_state", state, + ) if a.agentConfig.stateCallback != nil { go a.agentConfig.stateCallback(a.state) @@ -296,7 +301,7 @@ func (a *agent) updateState(state AgentState) (AgentState, error) { // Start starts an agent returning after successfully connecting and sending // the first heartbeat. func (a *agent) Start(ctx context.Context) error { - a.log.Debugf("Starting agent %v", a.addr) + a.logger.DebugContext(ctx, "Starting agent", "addr", a.addr.FullAddress()) var err error defer func() { @@ -325,7 +330,7 @@ func (a *agent) Start(ctx context.Context) error { a.wg.Add(1) go func() { if err := a.handleGlobalRequests(a.ctx, a.client.GlobalRequests()); err != nil { - a.log.WithError(err).Debug("Failed to handle global requests.") + a.logger.DebugContext(a.ctx, "Failed to handle global requests", "error", err) } a.wg.Done() a.Stop() @@ -336,7 +341,7 @@ func (a *agent) Start(ctx context.Context) error { a.wg.Add(1) go func() { if err := a.handleDrainChannels(); err != nil { - a.log.WithError(err).Debug("Failed to handle drainable channels.") + a.logger.DebugContext(a.ctx, "Failed to handle drainable channels", "error", err) } a.wg.Done() a.Stop() @@ -345,7 +350,7 @@ func (a *agent) Start(ctx context.Context) error { a.wg.Add(1) go func() { if err := a.handleChannels(); err != nil { - a.log.WithError(err).Debug("Failed to handle channels.") + a.logger.DebugContext(a.ctx, "Failed to handle channels", "error", err) } a.wg.Done() a.Stop() @@ -460,23 +465,23 @@ func (a *agent) handleGlobalRequests(ctx context.Context, requests <-chan *ssh.R case versionRequest: version, err := a.versionGetter.getVersion(ctx) if err != nil { - a.log.WithError(err).Warnf("Failed to retrieve auth version in response to %v request.", r.Type) + a.logger.WarnContext(ctx, "Failed to retrieve auth version in response to x-teleport-version request", "error", err) if err := a.client.Reply(r, false, []byte("Failed to retrieve auth version")); err != nil { - a.log.Debugf("Failed to reply to %v request: %v.", r.Type, err) + a.logger.DebugContext(ctx, "Failed to reply to x-teleport-version request", "error", err) continue } } if err := a.client.Reply(r, true, []byte(version)); err != nil { - a.log.Debugf("Failed to reply to %v request: %v.", r.Type, err) + a.logger.DebugContext(ctx, "Failed to reply to x-teleport-version request", "error", err) continue } case reconnectRequest: - a.log.Debugf("Received reconnect advisory request from proxy.") + a.logger.DebugContext(ctx, "Received reconnect advisory request from proxy") if r.WantReply { err := a.client.Reply(r, true, nil) if err != nil { - a.log.Debugf("Failed to reply to %v request: %v.", r.Type, err) + a.logger.DebugContext(ctx, "Failed to reply to reconnect@goteleport.com request", "error", err) } } @@ -487,7 +492,7 @@ func (a *agent) handleGlobalRequests(ctx context.Context, requests <-chan *ssh.R // This handles keep-alive messages and matches the behavior of OpenSSH. err := a.client.Reply(r, false, nil) if err != nil { - a.log.Debugf("Failed to reply to %v request: %v.", r.Type, err) + a.logger.DebugContext(ctx, "Failed to reply to global request", "request_type", r.Type, "error", err) continue } } @@ -555,10 +560,10 @@ func (a *agent) handleDrainChannels() error { bytes, _ := a.clock.Now().UTC().MarshalText() _, err := a.hbChannel.SendRequest(a.ctx, "ping", false, bytes) if err != nil { - a.log.Error(err) + a.logger.ErrorContext(a.ctx, "failed to send ping request", "error", err) return trace.Wrap(err) } - a.log.Debugf("Ping -> %v.", a.client.RemoteAddr()) + a.logger.DebugContext(a.ctx, "Sent ping request", "target_addr", logutils.StringerAttr(a.client.RemoteAddr())) // Handle transport requests. case nch := <-a.transportC: if nch == nil { @@ -567,15 +572,15 @@ func (a *agent) handleDrainChannels() error { if a.isDraining() { err := nch.Reject(ssh.ConnectionFailed, "agent connection is draining") if err != nil { - a.log.WithError(err).Warningf("Failed to reject transport channel.") + a.logger.WarnContext(a.ctx, "Failed to reject transport channel", "error", err) } continue } - a.log.Debugf("Transport request: %v.", nch.ChannelType()) + a.logger.DebugContext(a.ctx, "Received trransport request", "channel_type", nch.ChannelType()) ch, req, err := nch.Accept() if err != nil { - a.log.Warningf("Failed to accept transport request: %v.", err) + a.logger.WarnContext(a.ctx, "Failed to accept transport request", "error", err) continue } @@ -601,10 +606,10 @@ func (a *agent) handleChannels() error { if nch == nil { continue } - a.log.Debugf("Discovery request channel opened: %v.", nch.ChannelType()) + a.logger.DebugContext(a.ctx, "Discovery request channel opened", "channel_type", nch.ChannelType()) ch, req, err := nch.Accept() if err != nil { - a.log.Warningf("Failed to accept discovery channel request: %v.", err) + a.logger.WarnContext(a.ctx, "Failed to accept discovery channel request", "error", err) continue } @@ -624,11 +629,11 @@ func (a *agent) handleChannels() error { // ch : SSH channel which received "teleport-transport" out-of-band request // reqC : request payload func (a *agent) handleDiscovery(ch ssh.Channel, reqC <-chan *ssh.Request) { - a.log.Debugf("handleDiscovery requests channel.") + a.logger.DebugContext(a.ctx, "handleDiscovery requests channel") sshutils.DiscardChannelData(ch) defer func() { if err := ch.Close(); err != nil { - a.log.Warnf("Failed to close discovery channel: %v", err) + a.logger.WarnContext(a.ctx, "Failed to close discovery channel", "error", err) } }() @@ -639,17 +644,17 @@ func (a *agent) handleDiscovery(ch ssh.Channel, reqC <-chan *ssh.Request) { return case req = <-reqC: if req == nil { - a.log.Infof("Connection closed, returning") + a.logger.InfoContext(a.ctx, "Connection closed, returning") return } var r discoveryRequest if err := json.Unmarshal(req.Payload, &r); err != nil { - a.log.WithError(err).Warn("Bad payload") + a.logger.WarnContext(a.ctx, "Received discovery request with bad payload", "error", err) return } - a.log.Debugf("Received discovery request: %s", &r) + a.logger.DebugContext(a.ctx, "Received discovery request", "discovery_request", logutils.StringerAttr(&r)) a.tracker.TrackExpected(r.TrackProxies()...) } } diff --git a/lib/reversetunnel/agent_dialer.go b/lib/reversetunnel/agent_dialer.go index 56c710733a343..01f79397c3dff 100644 --- a/lib/reversetunnel/agent_dialer.go +++ b/lib/reversetunnel/agent_dialer.go @@ -20,10 +20,10 @@ package reversetunnel import ( "context" + "log/slog" "strings" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" apidefaults "github.com/gravitational/teleport/api/defaults" @@ -55,7 +55,7 @@ type agentDialer struct { authMethods []ssh.AuthMethod fips bool options []proxy.DialerOptionFunc - log logrus.FieldLogger + logger *slog.Logger isClaimed func(principals ...string) bool } @@ -65,7 +65,7 @@ func (d *agentDialer) DialContext(ctx context.Context, addr utils.NetAddr) (SSHC dialer := proxy.DialerFromEnvironment(addr.Addr, d.options...) pconn, err := dialer.DialTimeout(ctx, addr.AddrNetwork, addr.Addr, apidefaults.DefaultIOTimeout) if err != nil { - d.log.WithError(err).Debugf("Failed to dial %s.", addr.Addr) + d.logger.DebugContext(ctx, "Failed to dial", "error", err, "target_addr", addr.Addr) return nil, trace.Wrap(err) } @@ -75,7 +75,7 @@ func (d *agentDialer) DialContext(ctx context.Context, addr utils.NetAddr) (SSHC GetHostCheckers: d.hostCheckerFunc(ctx), OnCheckCert: func(c *ssh.Certificate) error { if d.isClaimed != nil && d.isClaimed(c.ValidPrincipals...) { - d.log.Debugf("Aborting SSH handshake because the proxy %q is already claimed by some other agent.", c.ValidPrincipals[0]) + d.logger.DebugContext(ctx, "Aborting SSH handshake because the proxy is already claimed by some other agent.", "proxy_id", c.ValidPrincipals[0]) // the error message must end with // [proxyAlreadyClaimedError] to be recognized by // [isProxyAlreadyClaimed] @@ -88,7 +88,7 @@ func (d *agentDialer) DialContext(ctx context.Context, addr utils.NetAddr) (SSHC FIPS: d.fips, }) if err != nil { - d.log.Debugf("Failed to create host key callback for %v: %v.", addr.Addr, err) + d.logger.DebugContext(ctx, "Failed to create host key callback", "target_addr", addr.Addr, "error", err) return nil, trace.Wrap(err) } diff --git a/lib/reversetunnel/agent_dialer_test.go b/lib/reversetunnel/agent_dialer_test.go index 2293c7b7a2620..ec912a51fa690 100644 --- a/lib/reversetunnel/agent_dialer_test.go +++ b/lib/reversetunnel/agent_dialer_test.go @@ -23,7 +23,6 @@ import ( "testing" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh" @@ -90,7 +89,7 @@ func TestAgentCertChecker(t *testing.T) { dialer := agentDialer{ client: &fakeClient{caKey: ca.PublicKey()}, authMethods: []ssh.AuthMethod{ssh.PublicKeys(signer)}, - log: logrus.New(), + logger: utils.NewSlogLoggerForTests(), } _, err = dialer.DialContext(context.Background(), *utils.MustParseAddr(sshServer.Addr())) diff --git a/lib/reversetunnel/agentpool.go b/lib/reversetunnel/agentpool.go index 25a59cc1cdebe..c4ea000758570 100644 --- a/lib/reversetunnel/agentpool.go +++ b/lib/reversetunnel/agentpool.go @@ -25,13 +25,13 @@ import ( "errors" "fmt" "io" + "log/slog" "net" "sync" "time" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" @@ -94,7 +94,7 @@ type AgentPool struct { // backoff limits the rate at which new agents are created. backoff retryutils.Retry - log logrus.FieldLogger + logger *slog.Logger } // AgentPoolConfig holds configuration parameters for the agent pool @@ -201,13 +201,11 @@ func NewAgentPool(ctx context.Context, config AgentPoolConfig) (*AgentPool, erro active: newAgentStore(), events: make(chan Agent), backoff: retry, - log: logrus.WithFields(logrus.Fields{ - teleport.ComponentKey: teleport.ComponentReverseTunnelAgent, - teleport.ComponentFields: logrus.Fields{ - "targetCluster": config.Cluster, - "localCluster": config.LocalCluster, - }, - }), + logger: slog.With( + teleport.ComponentKey, teleport.ComponentReverseTunnelAgent, + "target_cluster", config.Cluster, + "local_cluster", config.LocalCluster, + ), runtimeConfig: newAgentPoolRuntimeConfig(), } @@ -239,7 +237,7 @@ func (p *AgentPool) updateConnectedProxies() { } proxies := p.active.proxyIDs() - p.log.Debugf("Updating connected proxies: %v", proxies) + p.logger.DebugContext(p.ctx, "Updating connected proxies", "proxies", proxies) p.AgentPoolConfig.ConnectedProxyGetter.setProxyIDs(proxies) } @@ -250,12 +248,15 @@ func (p *AgentPool) Count() int { // Start starts the agent pool in the background. func (p *AgentPool) Start() error { - p.log.Debugf("Starting agent pool %s.%s...", p.HostUUID, p.Cluster) + p.logger.DebugContext(p.ctx, "Starting agent pool", + "host_uuid", p.HostUUID, + "cluster", p.Cluster, + ) p.wg.Add(1) go func() { if err := p.run(); err != nil { - p.log.WithError(err).Warn("Agent pool exited.") + p.logger.WarnContext(p.ctx, "Agent pool exited", "error", err) } p.cancel() @@ -274,9 +275,9 @@ func (p *AgentPool) run() error { } else if isProxyAlreadyClaimed(err) { // "proxy already claimed" is a fairly benign error, we should not // spam the log with stack traces for it - p.log.Debugf("Failed to connect agent: %v.", err) + p.logger.DebugContext(p.ctx, "Failed to connect agent", "error", err) } else { - p.log.WithError(err).Debugf("Failed to connect agent.") + p.logger.DebugContext(p.ctx, "Failed to connect agent", "error", err) } } else { p.wg.Add(1) @@ -288,7 +289,7 @@ func (p *AgentPool) run() error { if p.ctx.Err() != nil { return nil } else if err != nil { - p.log.WithError(err).Debugf("Failed to wait for backoff.") + p.logger.DebugContext(p.ctx, "Failed to wait for backoff", "error", err) } } } @@ -337,7 +338,10 @@ func (p *AgentPool) updateRuntimeConfig(ctx context.Context) error { restrictConnectionCount := p.runtimeConfig.restrictConnectionCount() connectionCount := p.runtimeConfig.getConnectionCount() - p.log.Debugf("Runtime config: restrict_connection_count: %v connection_count: %v", restrictConnectionCount, connectionCount) + p.logger.DebugContext(ctx, "Runtime config updated", + "restrict_connection_count", restrictConnectionCount, + "connection_count", connectionCount, + ) if restrictConnectionCount { p.tracker.SetConnectionCount(connectionCount) @@ -420,7 +424,7 @@ func (p *AgentPool) handleEvent(ctx context.Context, agent Agent) { } } p.updateConnectedProxies() - p.log.Debugf("Active agent count: %d", p.active.len()) + p.logger.DebugContext(ctx, "Processed agent event", "active_agent_count", p.active.len()) } // stateCallback adds events to the queue for each agent state change. @@ -444,7 +448,7 @@ func (p *AgentPool) newAgent(ctx context.Context, tracker *track.Tracker, lease err = p.runtimeConfig.updateRemote(ctx, addr) if err != nil { - p.log.WithError(err).Debugf("Failed to update remote config.") + p.logger.DebugContext(ctx, "Failed to update remote config", "error", err) } options := []proxy.DialerOptionFunc{proxy.WithInsecureSkipTLSVerify(lib.IsInsecureDevMode())} @@ -458,7 +462,7 @@ func (p *AgentPool) newAgent(ctx context.Context, tracker *track.Tracker, lease authMethods: p.AuthMethods, options: options, username: p.HostUUID, - log: p.log, + logger: p.logger, isClaimed: p.tracker.IsClaimed, } @@ -471,7 +475,7 @@ func (p *AgentPool) newAgent(ctx context.Context, tracker *track.Tracker, lease tracker: tracker, lease: lease, clock: p.Clock, - log: p.log, + logger: p.logger, localAuthAddresses: p.LocalAuthAddresses, proxySigner: p.PROXYSigner, }) @@ -536,7 +540,7 @@ func (p *AgentPool) handleTransport(ctx context.Context, channel ssh.Channel, re sconn: conn, channel: channel, requestCh: requests, - log: p.log, + logger: p.logger, authServers: p.LocalAuthAddresses, proxySigner: p.PROXYSigner, forwardClientAddress: true, @@ -566,7 +570,7 @@ func (p *AgentPool) handleLocalTransport(ctx context.Context, channel ssh.Channe return case <-time.After(apidefaults.DefaultIOTimeout): go ssh.DiscardRequests(reqC) - p.log.Warn("Timed out waiting for transport dial request.") + p.logger.WarnContext(ctx, "Timed out waiting for transport dial request") return case r, ok := <-reqC: if !ok { @@ -579,14 +583,14 @@ func (p *AgentPool) handleLocalTransport(ctx context.Context, channel ssh.Channe // sconn should never be nil, but it's sourced from the agent state and // starts as nil, and the original transport code checked against it if sconn == nil || p.Server == nil { - p.log.Error("Missing client or server (this is a bug).") + p.logger.ErrorContext(ctx, "Missing client or server (this is a bug)") fmt.Fprintf(channel.Stderr(), "internal server error") req.Reply(false, nil) return } if err := req.Reply(true, nil); err != nil { - p.log.Errorf("Failed to respond to dial request: %v.", err) + p.logger.ErrorContext(ctx, "Failed to respond to dial request", "error", err) return } @@ -596,8 +600,9 @@ func (p *AgentPool) handleLocalTransport(ctx context.Context, channel ssh.Channe switch dialReq.Address { case reversetunnelclient.LocalNode, reversetunnelclient.LocalKubernetes, reversetunnelclient.LocalWindowsDesktop: default: - p.log.WithField("address", dialReq.Address). - Warn("Received dial request for unexpected address, routing to the local service anyway.") + p.logger.WarnContext(ctx, "Received dial request for unexpected address, routing to the local service anyway", + "dial_addr", dialReq.Address, + ) } if src, err := utils.ParseAddr(dialReq.ClientSrcAddr); err == nil { conn = utils.NewConnWithSrcAddr(conn, getTCPAddr(src)) @@ -768,7 +773,10 @@ func (c *agentPoolRuntimeConfig) updateRemote(ctx context.Context, addr *utils.N c.remoteTLSRoutingEnabled = tlsRoutingEnabled if c.remoteTLSRoutingEnabled { c.tlsRoutingConnUpgradeRequired = client.IsALPNConnUpgradeRequired(ctx, addr.Addr, lib.IsInsecureDevMode()) - logrus.Debugf("ALPN upgrade required for remote %v: %v", addr.Addr, c.tlsRoutingConnUpgradeRequired) + slog.DebugContext(ctx, "ALPN upgrade required for remote cluster", + "remot_addr", addr.Addr, + "conn_upgrade_required", c.tlsRoutingConnUpgradeRequired, + ) } return nil } @@ -802,7 +810,7 @@ func (c *agentPoolRuntimeConfig) update(ctx context.Context, netConfig types.Clu if err == nil { c.tlsRoutingConnUpgradeRequired = client.IsALPNConnUpgradeRequired(ctx, addr.Addr, lib.IsInsecureDevMode()) } else { - logrus.WithError(err).Warnf("Failed to resolve addr.") + slog.WarnContext(ctx, "Failed to resolve addr", "error", err) } } } diff --git a/lib/reversetunnel/conn.go b/lib/reversetunnel/conn.go index b78f1b97d9378..8a678690f97cd 100644 --- a/lib/reversetunnel/conn.go +++ b/lib/reversetunnel/conn.go @@ -19,8 +19,10 @@ package reversetunnel import ( + "context" "encoding/json" "fmt" + "log/slog" "net" "sync" "sync/atomic" @@ -28,12 +30,12 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/sshutils" + logutils "github.com/gravitational/teleport/lib/utils/log" ) // connKey is a key used to identify tunnel connections. It contains the UUID @@ -54,8 +56,8 @@ type remoteConn struct { lastHeartbeat atomic.Int64 *connConfig - mu sync.Mutex - log *logrus.Entry + mu sync.Mutex + logger *slog.Logger // discoveryCh is the SSH channel over which discovery requests are sent. discoveryCh ssh.Channel @@ -109,9 +111,7 @@ type connConfig struct { func newRemoteConn(cfg *connConfig) *remoteConn { c := &remoteConn{ - log: logrus.WithFields(logrus.Fields{ - teleport.ComponentKey: "discovery", - }), + logger: slog.With(teleport.ComponentKey, "discovery"), connConfig: cfg, clock: clockwork.NewRealClock(), newProxiesC: make(chan []types.Server, 100), @@ -181,7 +181,11 @@ func (c *remoteConn) markInvalid(err error) { c.lastError = err c.invalid.Store(true) - c.log.Warnf("Unhealthy connection to %v %v: %v.", c.clusterName, c.conn.RemoteAddr(), err) + c.logger.WarnContext(context.Background(), "Unhealthy reverse tunnel connection", + "cluster", c.clusterName, + "remote_addr", logutils.StringerAttr(c.conn.RemoteAddr()), + "error", err, + ) } func (c *remoteConn) markValid() { @@ -256,7 +260,7 @@ func (c *remoteConn) updateProxies(proxies []types.Server) { default: // Missing proxies update is no longer critical with more permissive // discovery protocol that tolerates conflicting, stale or missing updates - c.log.Warnf("Discovery channel overflow at %v.", len(c.newProxiesC)) + c.logger.WarnContext(context.Background(), "Discovery channel overflow", "new_proxy_count", len(c.newProxiesC)) } } @@ -267,7 +271,7 @@ func (c *remoteConn) adviseReconnect() error { // sendDiscoveryRequest sends a discovery request with up to date // list of connected proxies -func (c *remoteConn) sendDiscoveryRequest(req discoveryRequest) error { +func (c *remoteConn) sendDiscoveryRequest(ctx context.Context, req discoveryRequest) error { discoveryCh, err := c.openDiscoveryChannel() if err != nil { return trace.Wrap(err) @@ -282,7 +286,10 @@ func (c *remoteConn) sendDiscoveryRequest(req discoveryRequest) error { // Log the discovery request being sent. Useful for debugging to know what // proxies the tunnel server thinks exist. - c.log.Debugf("Sending discovery request with proxies %v to %v.", req.ProxyNames(), c.sconn.RemoteAddr()) + c.logger.DebugContext(ctx, "Sending discovery request", + "proxies", req.ProxyNames(), + "target_addr", logutils.StringerAttr(c.sconn.RemoteAddr()), + ) if _, err := discoveryCh.SendRequest(chanDiscoveryReq, false, payload); err != nil { c.markInvalid(err) diff --git a/lib/reversetunnel/localsite.go b/lib/reversetunnel/localsite.go index 7c89ea25273b0..3446a882cec23 100644 --- a/lib/reversetunnel/localsite.go +++ b/lib/reversetunnel/localsite.go @@ -21,6 +21,7 @@ package reversetunnel import ( "context" "fmt" + "log/slog" "net" "slices" "sync" @@ -29,7 +30,6 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" @@ -48,6 +48,7 @@ import ( "github.com/gravitational/teleport/lib/srv/forward" "github.com/gravitational/teleport/lib/teleagent" "github.com/gravitational/teleport/lib/utils" + logutils "github.com/gravitational/teleport/lib/utils/log" proxyutils "github.com/gravitational/teleport/lib/utils/proxy" ) @@ -102,12 +103,10 @@ func newLocalSite(srv *server, domainName string, authServers []string, opts ... authServers: authServers, remoteConns: make(map[connKey][]*remoteConn), clock: srv.Clock, - log: log.WithFields(log.Fields{ - teleport.ComponentKey: teleport.ComponentReverseTunnelServer, - teleport.ComponentFields: map[string]string{ - "cluster": domainName, - }, - }), + logger: slog.With( + teleport.ComponentKey, teleport.ComponentReverseTunnelServer, + "cluster", domainName, + ), offlineThreshold: srv.offlineThreshold, peerClient: srv.PeerClient, periodicFunctionInterval: periodicFunctionInterval, @@ -129,7 +128,7 @@ func newLocalSite(srv *server, domainName string, authServers []string, opts ... // // it implements RemoteSite interface type localSite struct { - log log.FieldLogger + logger *slog.Logger domainName string authServers []string srv *server @@ -292,13 +291,15 @@ func (s *localSite) maybeSendSignedPROXYHeader(params reversetunnelclient.DialPa // TODO(awly): unit test this func (s *localSite) DialTCP(params reversetunnelclient.DialParams) (net.Conn, error) { - s.log.Debugf("Dialing %v.", params) + ctx := s.srv.ctx + logger := s.logger.With("dial_params", logutils.StringerAttr(params)) + logger.DebugContext(ctx, "Initiating dia request") conn, useTunnel, err := s.getConn(params) if err != nil { return nil, trace.Wrap(err) } - s.log.Debugf("Succeeded dialing %v.", params) + logger.DebugContext(ctx, "Succeeded dialing") if err := s.maybeSendSignedPROXYHeader(params, conn, useTunnel); err != nil { return nil, trace.Wrap(err) @@ -320,12 +321,12 @@ func (s *localSite) adviseReconnect(ctx context.Context) { s.remoteConnsMtx.Lock() for _, conns := range s.remoteConns { for _, conn := range conns { - s.log.Debugf("Sending reconnect: %s", conn.nodeID) + s.logger.DebugContext(ctx, "Sending reconnect to server ", "server_id", conn.nodeID) wg.Add(1) go func(conn *remoteConn) { if err := conn.adviseReconnect(); err != nil { - s.log.WithError(err).Warn("Failed sending reconnect advisory") + s.logger.WarnContext(ctx, "Failed sending reconnect advisory", "error", err) } wg.Done() }(conn) @@ -346,10 +347,15 @@ func (s *localSite) adviseReconnect(ctx context.Context) { } func (s *localSite) dialAndForward(params reversetunnelclient.DialParams) (_ net.Conn, retErr error) { + ctx := s.srv.ctx + if params.GetUserAgent == nil && !params.IsAgentlessNode { return nil, trace.BadParameter("agentless node require an agent getter") } - s.log.Debugf("Dialing and forwarding from %v to %v.", params.From, params.To) + s.logger.DebugContext(ctx, "Initiating dial and forwarding request", + "source_addr", logutils.StringerAttr(params.From), + "target_addr", logutils.StringerAttr(params.To), + ) // request user agent connection if a SSH user agent is set var userAgent teleagent.Agent @@ -378,7 +384,7 @@ func (s *localSite) dialAndForward(params reversetunnelclient.DialParams) (_ net } // Get a host certificate for the forwarding node from the cache. - hostCertificate, err := s.certificateCache.getHostCertificate(context.TODO(), params.Address, params.Principals) + hostCertificate, err := s.certificateCache.getHostCertificate(ctx, params.Address, params.Principals) if err != nil { return nil, trace.Wrap(err) } @@ -438,7 +444,10 @@ func (s *localSite) dialTunnel(dreq *sshutils.DialReq) (net.Conn, error) { return nil, trace.NotFound("no tunnel connection found: %v", err) } - s.log.Debugf("Tunnel dialing to %v, client source %v", dreq.ServerID, dreq.ClientSrcAddr) + s.logger.DebugContext(s.srv.ctx, "Tunnel dialing to host", + "target_host_id", dreq.ServerID, + "src_addr", dreq.ClientSrcAddr, + ) conn, err := s.chanTransportConn(rconn, dreq) if err != nil { @@ -607,7 +616,7 @@ func (s *localSite) getConn(params reversetunnelclient.DialParams) (conn net.Con peeringEnabled := s.tryProxyPeering(params) if peeringEnabled { - s.log.Info("Dialing over peer proxy") + s.logger.InfoContext(s.srv.ctx, "Dialing over peer proxy") conn, peerErr = s.peerClient.DialNode( params.ProxyIDs, params.ServerID, params.From, params.To, params.ConnType, ) @@ -645,7 +654,7 @@ func (s *localSite) getConn(params reversetunnelclient.DialParams) (conn net.Con dialTimeout := apidefaults.DefaultIOTimeout if cnc, err := s.accessPoint.GetClusterNetworkingConfig(s.srv.Context); err != nil { - s.log.WithError(err).Warn("Failed to get cluster networking config - using default dial timeout") + s.logger.WarnContext(s.srv.ctx, "Failed to get cluster networking config - using default dial timeout", "error", err) } else { dialTimeout = cnc.GetSSHDialTimeout() } @@ -653,7 +662,12 @@ func (s *localSite) getConn(params reversetunnelclient.DialParams) (conn net.Con conn, directErr = dialer.DialTimeout(s.srv.Context, params.To.Network(), params.To.String(), dialTimeout) if directErr != nil { directMsg := getTunnelErrorMessage(params, "direct dial", directErr) - s.log.WithField("address", params.To.String()).Debugf("All attempted dial methods failed. tunnel=%q, peer=%q, direct=%q", tunnelErr, peerErr, directErr) + s.logger.DebugContext(s.srv.ctx, "All attempted dial methods failed", + "target_addr", logutils.StringerAttr(params.To), + "tunnel_error", tunnelErr, + "peer_error", peerErr, + "direct_error", directErr, + ) aggregateErr := trace.NewAggregate(tunnelErr, peerErr, directErr) return nil, false, trace.ConnectionProblem(aggregateErr, directMsg) } @@ -701,29 +715,29 @@ func (s *localSite) fanOutProxies(proxies []types.Server) { // handleHeartbeat receives heartbeat messages from the connected agent // if the agent has missed several heartbeats in a row, Proxy marks // the connection as invalid. -func (s *localSite) handleHeartbeat(rconn *remoteConn, ch ssh.Channel, reqC <-chan *ssh.Request) { +func (s *localSite) handleHeartbeat(ctx context.Context, rconn *remoteConn, ch ssh.Channel, reqC <-chan *ssh.Request) { sshutils.DiscardChannelData(ch) if ch != nil { defer func() { if err := ch.Close(); err != nil { - s.log.Warnf("Failed to close heartbeat channel: %v", err) + s.logger.WarnContext(ctx, "Failed to close heartbeat channel", "error", err) } }() } - logger := s.log.WithFields(log.Fields{ - "serverID": rconn.nodeID, - "addr": rconn.conn.RemoteAddr().String(), - }) + logger := s.logger.With( + "server_id", rconn.nodeID, + "addr", logutils.StringerAttr(rconn.conn.RemoteAddr()), + ) firstHeartbeat := true proxyResyncTicker := s.clock.NewTicker(s.proxySyncInterval) defer func() { proxyResyncTicker.Stop() - logger.Warn("Closing remote connection to agent.") + logger.WarnContext(ctx, "Closing remote connection to agent") s.removeRemoteConn(rconn) if err := rconn.Close(); err != nil && !utils.IsOKNetworkError(err) { - logger.WithError(err).Warn("Failed to close remote connection") + logger.WarnContext(ctx, "Failed to close remote connection", "error", err) } if !firstHeartbeat { reverseSSHTunnels.WithLabelValues(rconn.tunnelType).Dec() @@ -735,18 +749,18 @@ func (s *localSite) handleHeartbeat(rconn *remoteConn, ch ssh.Channel, reqC <-ch for { select { case <-s.srv.ctx.Done(): - logger.Info("Closing") + logger.InfoContext(ctx, "Closing") return case <-proxyResyncTicker.Chan(): var req discoveryRequest - proxies, err := s.srv.proxyWatcher.CurrentResources(s.srv.ctx) + proxies, err := s.srv.proxyWatcher.CurrentResources(ctx) if err != nil { - logger.WithError(err).Warn("Failed to get proxy set") + logger.WarnContext(ctx, "Failed to get proxy set", "error", err) } req.SetProxies(proxies) - if err := rconn.sendDiscoveryRequest(req); err != nil { - logger.WithError(err).Debug("Marking connection invalid on error") + if err := rconn.sendDiscoveryRequest(ctx, req); err != nil { + logger.DebugContext(ctx, "Marking connection invalid on error", "error", err) rconn.markInvalid(err) return } @@ -754,14 +768,14 @@ func (s *localSite) handleHeartbeat(rconn *remoteConn, ch ssh.Channel, reqC <-ch var req discoveryRequest req.SetProxies(proxies) - if err := rconn.sendDiscoveryRequest(req); err != nil { - logger.WithError(err).Debug("Failed to send discovery request to agent") + if err := rconn.sendDiscoveryRequest(ctx, req); err != nil { + logger.DebugContext(ctx, "Failed to send discovery request to agent", "error", err) rconn.markInvalid(err) return } case req := <-reqC: if req == nil { - logger.Debug("Agent disconnected.") + logger.DebugContext(ctx, "Agent disconnected") rconn.markInvalid(trace.ConnectionProblem(nil, "agent disconnected")) return } @@ -770,7 +784,7 @@ func (s *localSite) handleHeartbeat(rconn *remoteConn, ch ssh.Channel, reqC <-ch // send it the list of current proxies back proxies, err := s.srv.proxyWatcher.CurrentResources(s.srv.ctx) if err != nil { - logger.WithError(err).Warn("Failed to get proxy set") + logger.WarnContext(ctx, "Failed to get proxy set", "error", err) } if len(proxies) > 0 { rconn.updateProxies(proxies) @@ -788,9 +802,9 @@ func (s *localSite) handleHeartbeat(rconn *remoteConn, ch ssh.Channel, reqC <-ch log := logger if roundtrip != 0 { - log = logger.WithField("latency", roundtrip.String()) + log = logger.With("latency", logutils.StringerAttr(roundtrip)) } - log.Debugf("Ping <- %v", rconn.conn.RemoteAddr()) + log.DebugContext(ctx, "Received ping request", "remote_addr", logutils.StringerAttr(rconn.conn.RemoteAddr())) rconn.setLastHeartbeat(s.clock.Now().UTC()) rconn.markValid() @@ -799,10 +813,10 @@ func (s *localSite) handleHeartbeat(rconn *remoteConn, ch ssh.Channel, reqC <-ch // terminate and remove the connection if offline, otherwise warn and wait for the next heartbeat if rconn.isOffline(t, s.offlineThreshold*missedHeartBeatThreshold) { - logger.Errorf("Closing unhealthy and idle connection. Heartbeat last received at %s", rconn.getLastHeartbeat()) + logger.ErrorContext(ctx, "Closing unhealthy and idle connection", "last_heartbeat", rconn.getLastHeartbeat()) return } - logger.Warnf("Deferring closure of unhealthy connection due to %d active connections", rconn.activeSessions()) + logger.WarnContext(ctx, "Deferring closure of unhealthy connection due to active connections", "active_conn_count", rconn.activeSessions()) offlineThresholdTimer.Reset(s.offlineThreshold) continue @@ -878,7 +892,7 @@ func (s *localSite) getRemoteConn(dreq *sshutils.DialReq) (*remoteConn, error) { } func (s *localSite) chanTransportConn(rconn *remoteConn, dreq *sshutils.DialReq) (net.Conn, error) { - s.log.Debugf("Connecting to %v through tunnel.", rconn.conn.RemoteAddr()) + s.logger.DebugContext(s.srv.ctx, "Connecting to target through tunnel", "target_addr", logutils.StringerAttr(rconn.conn.RemoteAddr())) conn, markInvalid, err := sshutils.ConnectProxyTransport(rconn.sconn, dreq, false) if err != nil { @@ -934,7 +948,7 @@ func (s *localSite) periodicFunctions() { return case <-ticker.Chan(): if err := s.sshTunnelStats(); err != nil { - s.log.Warningf("Failed to report SSH tunnel statistics for: %v: %v.", s.domainName, err) + s.logger.WarnContext(s.srv.ctx, "Failed to report SSH tunnel statistics ", "cluster", s.domainName, "error", err) } } } @@ -988,7 +1002,11 @@ func (s *localSite) sshTunnelStats() error { if n > 10 { n = 10 } - s.log.Debugf("Cluster %v is missing %v tunnels. A small number of missing tunnels is normal, for example, a node could have just been shut down, the proxy restarted, etc. However, if this error persists with an elevated number of missing tunnels, it often indicates nodes can not discover all registered proxies. Check that all of your proxies are behind a load balancer and the load balancer is using a round robin strategy. Some of the missing hosts: %v.", s.domainName, len(missing), missing[:n]) + s.logger.DebugContext(s.srv.ctx, "Cluster is missing some tunnels. A small number of missing tunnels is normal, for example, a node could have just been shut down, the proxy restarted, etc. However, if this error persists with an elevated number of missing tunnels, it often indicates nodes can not discover all registered proxies. Check that all of your proxies are behind a load balancer and the load balancer is using a round robin strategy", + "cluster", s.domainName, + "missing_count", len(missing), + "missing", missing[:n], + ) } return nil } diff --git a/lib/reversetunnel/localsite_test.go b/lib/reversetunnel/localsite_test.go index 195a1e76510c2..543ecfd894c2e 100644 --- a/lib/reversetunnel/localsite_test.go +++ b/lib/reversetunnel/localsite_test.go @@ -77,7 +77,7 @@ func TestRemoteConnCleanup(t *testing.T) { ctx: ctx, Config: Config{Clock: clock}, localAuthClient: &mockLocalSiteClient{}, - log: utils.NewLoggerForTests(), + logger: utils.NewSlogLoggerForTests(), offlineThreshold: time.Second, proxyWatcher: watcher, } @@ -102,7 +102,7 @@ func TestRemoteConnCleanup(t *testing.T) { // terminated by too many missed heartbeats go func() { - site.handleHeartbeat(conn1, nil, reqs) + site.handleHeartbeat(ctx, conn1, nil, reqs) cancel() }() @@ -273,7 +273,7 @@ func TestProxyResync(t *testing.T) { ctx: ctx, Config: Config{Clock: clock}, localAuthClient: &mockLocalSiteClient{}, - log: utils.NewLoggerForTests(), + logger: utils.NewSlogLoggerForTests(), offlineThreshold: 24 * time.Hour, proxyWatcher: watcher, } @@ -312,7 +312,7 @@ func TestProxyResync(t *testing.T) { // terminated by canceled context go func() { - site.handleHeartbeat(conn1, nil, reqs) + site.handleHeartbeat(ctx, conn1, nil, reqs) }() expected := []types.Server{proxy1, proxy2} diff --git a/lib/reversetunnel/peer.go b/lib/reversetunnel/peer.go index 570be5edf4bbe..675ad71e4522a 100644 --- a/lib/reversetunnel/peer.go +++ b/lib/reversetunnel/peer.go @@ -26,7 +26,6 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - log "github.com/sirupsen/logrus" "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/types" @@ -155,14 +154,8 @@ func (p *clusterPeers) Close() error { return nil } // newClusterPeer returns new cluster peer func newClusterPeer(srv *server, connInfo types.TunnelConnection, offlineThreshold time.Duration) (*clusterPeer, error) { clusterPeer := &clusterPeer{ - srv: srv, - connInfo: connInfo, - log: log.WithFields(log.Fields{ - teleport.ComponentKey: teleport.ComponentReverseTunnelServer, - teleport.ComponentFields: map[string]string{ - "cluster": connInfo.GetClusterName(), - }, - }), + srv: srv, + connInfo: connInfo, clock: clockwork.NewRealClock(), offlineThreshold: offlineThreshold, } @@ -173,8 +166,6 @@ func newClusterPeer(srv *server, connInfo types.TunnelConnection, offlineThresho // clusterPeer is a remote cluster that has established // a tunnel to the peers type clusterPeer struct { - log *log.Entry - mu sync.Mutex connInfo types.TunnelConnection srv *server diff --git a/lib/reversetunnel/rc_manager.go b/lib/reversetunnel/rc_manager.go index 03db3f13f613f..f1e539ac3bf8b 100644 --- a/lib/reversetunnel/rc_manager.go +++ b/lib/reversetunnel/rc_manager.go @@ -20,12 +20,12 @@ package reversetunnel import ( "context" + "log/slog" "sync" "time" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" @@ -81,8 +81,8 @@ type RemoteClusterTunnelManagerConfig struct { KubeDialAddr utils.NetAddr // FIPS indicates if Teleport was started in FIPS mode. FIPS bool - // Log is the logger - Log logrus.FieldLogger + // Logger is the logger + Logger *slog.Logger // LocalAuthAddresses is a list of auth servers to use when dialing back to // the local cluster. LocalAuthAddresses []string @@ -109,8 +109,8 @@ func (c *RemoteClusterTunnelManagerConfig) CheckAndSetDefaults() error { if c.Clock == nil { c.Clock = clockwork.NewRealClock() } - if c.Log == nil { - c.Log = logrus.New() + if c.Logger == nil { + c.Logger = slog.Default() } return nil @@ -153,7 +153,7 @@ func (w *RemoteClusterTunnelManager) Run(ctx context.Context) { w.mu.Unlock() if err := w.Sync(ctx); err != nil { - w.cfg.Log.Warningf("Failed to sync reverse tunnels: %v.", err) + w.cfg.Logger.WarnContext(ctx, "Failed to sync reverse tunnels", "error", err) } ticker := time.NewTicker(defaults.ResyncInterval) @@ -162,11 +162,11 @@ func (w *RemoteClusterTunnelManager) Run(ctx context.Context) { for { select { case <-ctx.Done(): - w.cfg.Log.Debugf("Closing.") + w.cfg.Logger.DebugContext(ctx, "Closing") return case <-ticker.C: if err := w.Sync(ctx); err != nil { - w.cfg.Log.Warningf("Failed to sync reverse tunnels: %v.", err) + w.cfg.Logger.WarnContext(ctx, "Failed to sync reverse tunnels", "error", err) continue } } @@ -247,7 +247,7 @@ func realNewAgentPool(ctx context.Context, cfg RemoteClusterTunnelManagerConfig, } if err := pool.Start(); err != nil { - cfg.Log.WithError(err).Error("Failed to start agent pool") + cfg.Logger.ErrorContext(ctx, "Failed to start agent pool", "error", err) } return pool, nil diff --git a/lib/reversetunnel/remotesite.go b/lib/reversetunnel/remotesite.go index f9617f33b87d5..bfb3fa91412b4 100644 --- a/lib/reversetunnel/remotesite.go +++ b/lib/reversetunnel/remotesite.go @@ -23,13 +23,13 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "log/slog" "net" "sync" "time" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - log "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" @@ -46,6 +46,7 @@ import ( "github.com/gravitational/teleport/lib/srv/forward" "github.com/gravitational/teleport/lib/teleagent" "github.com/gravitational/teleport/lib/utils" + logutils "github.com/gravitational/teleport/lib/utils/log" ) // remoteSite is a remote site that established the inbound connection to @@ -54,7 +55,7 @@ import ( type remoteSite struct { sync.RWMutex - logger *log.Entry + logger *slog.Logger domainName string connections []*remoteConn lastUsed int @@ -115,7 +116,7 @@ func (s *remoteSite) getRemoteClient() (authclient.ClientI, bool, error) { // The fact that cluster has keys to remote CA means that the key exchange // has completed. - s.logger.Debug("Using TLS client to remote cluster.") + s.logger.DebugContext(s.ctx, "Using TLS client to remote cluster") tlsConfig := utils.TLSConfig(s.srv.ClientTLSCipherSuites) // encode the name of this cluster to identify this cluster, // connecting to the remote one (it is used to find the right certificate @@ -272,7 +273,7 @@ func (s *remoteSite) removeInvalidConns() { } else { go func(conn *remoteConn) { if err := conn.Close(); err != nil { - s.logger.WithError(err).Warn("Failed to close invalid connection") + s.logger.WarnContext(s.ctx, "Failed to close invalid connection", "error", err) } }(s.connections[i]) } @@ -305,12 +306,12 @@ func (s *remoteSite) adviseReconnect(ctx context.Context) { s.RLock() for _, conn := range s.connections { - s.logger.Debugf("Sending reconnect: %s", conn.nodeID) + s.logger.DebugContext(ctx, "Sending reconnect to server", "server_id", conn.nodeID) wg.Add(1) go func(conn *remoteConn) { if err := conn.adviseReconnect(); err != nil { - s.logger.WithError(err).Warn("Failed to send reconnection advisory") + s.logger.WarnContext(ctx, "Failed to send reconnection advisory", "error", err) } wg.Done() }(conn) @@ -365,7 +366,7 @@ func (s *remoteSite) registerHeartbeat(t time.Time) { s.setLastConnInfo(connInfo) err := s.localAccessPoint.UpsertTunnelConnection(connInfo) if err != nil { - s.logger.WithError(err).Warn("Failed to register heartbeat") + s.logger.WarnContext(s.ctx, "Failed to register heartbeat", "error", err) } } @@ -373,7 +374,7 @@ func (s *remoteSite) registerHeartbeat(t time.Time) { // that this node lost the connection and needs to be discovered func (s *remoteSite) deleteConnectionRecord() { if err := s.localAccessPoint.DeleteTunnelConnection(s.connInfo.GetClusterName(), s.connInfo.GetName()); err != nil { - s.logger.WithError(err).Warn("Failed to delete tunnel connection") + s.logger.WarnContext(s.ctx, "Failed to delete tunnel connection", "error", err) } } @@ -391,17 +392,17 @@ func (s *remoteSite) fanOutProxies(proxies []types.Server) { // handleHeartbeat receives heartbeat messages from the connected agent // if the agent has missed several heartbeats in a row, Proxy marks // the connection as invalid. -func (s *remoteSite) handleHeartbeat(conn *remoteConn, ch ssh.Channel, reqC <-chan *ssh.Request) { - logger := s.logger.WithFields(log.Fields{ - "serverID": conn.nodeID, - "addr": conn.conn.RemoteAddr().String(), - }) +func (s *remoteSite) handleHeartbeat(ctx context.Context, conn *remoteConn, ch ssh.Channel, reqC <-chan *ssh.Request) { + logger := s.logger.With( + "server_id", conn.nodeID, + "addr", logutils.StringerAttr(conn.conn.RemoteAddr()), + ) sshutils.DiscardChannelData(ch) if ch != nil { defer func() { if err := ch.Close(); err != nil { - logger.Warnf("Failed to close heartbeat channel: %v", err) + logger.WarnContext(ctx, "Failed to close heartbeat channel", "error", err) } }() } @@ -410,14 +411,14 @@ func (s *remoteSite) handleHeartbeat(conn *remoteConn, ch ssh.Channel, reqC <-ch proxyResyncTicker := s.clock.NewTicker(s.proxySyncInterval) defer func() { proxyResyncTicker.Stop() - logger.Info("Cluster connection closed.") + logger.InfoContext(ctx, "Cluster connection closed") if err := conn.Close(); err != nil && !utils.IsUseOfClosedNetworkError(err) { - logger.WithError(err).Warnf("Failed to close remote connection for remote site") + logger.WarnContext(ctx, "Failed to close remote connection for remote site", "error", err) } if err := s.srv.onSiteTunnelClose(s); err != nil { - logger.WithError(err).Warn("Failed to close remote site") + logger.WarnContext(ctx, "Failed to close remote site", "error", err) } }() @@ -426,18 +427,18 @@ func (s *remoteSite) handleHeartbeat(conn *remoteConn, ch ssh.Channel, reqC <-ch for { select { case <-s.ctx.Done(): - logger.Infof("closing") + logger.InfoContext(ctx, "closing") return case <-proxyResyncTicker.Chan(): var req discoveryRequest proxies, err := s.srv.proxyWatcher.CurrentResources(s.srv.ctx) if err != nil { - logger.WithError(err).Warn("Failed to get proxy set") + logger.WarnContext(ctx, "Failed to get proxy set", "error", err) } req.SetProxies(proxies) - if err := conn.sendDiscoveryRequest(req); err != nil { - logger.WithError(err).Debug("Marking connection invalid on error") + if err := conn.sendDiscoveryRequest(ctx, req); err != nil { + logger.DebugContext(ctx, "Marking connection invalid on error", "error", err) conn.markInvalid(err) return } @@ -445,17 +446,17 @@ func (s *remoteSite) handleHeartbeat(conn *remoteConn, ch ssh.Channel, reqC <-ch var req discoveryRequest req.SetProxies(proxies) - if err := conn.sendDiscoveryRequest(req); err != nil { - logger.WithError(err).Debug("Marking connection invalid on error") + if err := conn.sendDiscoveryRequest(ctx, req); err != nil { + logger.DebugContext(ctx, "Marking connection invalid on error", "error", err) conn.markInvalid(err) return } case req := <-reqC: if req == nil { - logger.Info("Cluster agent disconnected.") + logger.InfoContext(ctx, "Cluster agent disconnected") conn.markInvalid(trace.ConnectionProblem(nil, "agent disconnected")) if !s.HasValidConnections() { - logger.Debug("Deleting connection record.") + logger.DebugContext(ctx, "Deleting connection record") s.deleteConnectionRecord() } return @@ -463,9 +464,9 @@ func (s *remoteSite) handleHeartbeat(conn *remoteConn, ch ssh.Channel, reqC <-ch if firstHeartbeat { // as soon as the agent connects and sends a first heartbeat // send it the list of current proxies back - proxies, err := s.srv.proxyWatcher.CurrentResources(s.srv.ctx) + proxies, err := s.srv.proxyWatcher.CurrentResources(ctx) if err != nil { - logger.WithError(err).Warn("Failed to get proxy set") + logger.WarnContext(ctx, "Failed to get proxy set", "error", err) } if len(proxies) > 0 { conn.updateProxies(proxies) @@ -482,9 +483,9 @@ func (s *remoteSite) handleHeartbeat(conn *remoteConn, ch ssh.Channel, reqC <-ch pinglog := logger if roundtrip != 0 { - pinglog = pinglog.WithField("latency", roundtrip) + pinglog = pinglog.With("latency", roundtrip) } - pinglog.Debugf("Ping <- %v", conn.conn.RemoteAddr()) + pinglog.DebugContext(ctx, "Received ping request", "remote_addr", logutils.StringerAttr(conn.conn.RemoteAddr())) tm := s.clock.Now().UTC() conn.setLastHeartbeat(tm) @@ -498,11 +499,11 @@ func (s *remoteSite) handleHeartbeat(conn *remoteConn, ch ssh.Channel, reqC <-ch if t.After(hb.Add(s.offlineThreshold * missedHeartBeatThreshold)) { count := conn.activeSessions() if count == 0 { - logger.Errorf("Closing unhealthy and idle connection. Heartbeat last received at %s", hb) + logger.ErrorContext(ctx, "Closing unhealthy and idle connection", "last_heartbeat", hb) return } - logger.Warnf("Deferring closure of unhealthy connection due to %d active connections", count) + logger.WarnContext(ctx, "Deferring closure of unhealthy connection due to active connections", "active_conn_count", count) } offlineThresholdTimer.Reset(s.offlineThreshold) @@ -554,24 +555,24 @@ func (s *remoteSite) updateCertAuthorities(retry retryutils.Retry, remoteWatcher if err != nil { switch { case trace.IsNotFound(err): - s.logger.Debug("Remote cluster does not support cert authorities rotation yet.") + s.logger.DebugContext(s.ctx, "Remote cluster does not support cert authorities rotation yet") case trace.IsCompareFailed(err): - s.logger.Info("Remote cluster has updated certificate authorities, going to force reconnect.") + s.logger.InfoContext(s.ctx, "Remote cluster has updated certificate authorities, going to force reconnect") if err := s.srv.onSiteTunnelClose(&alwaysClose{RemoteSite: s}); err != nil { - s.logger.WithError(err).Warn("Failed to close remote site") + s.logger.WarnContext(s.ctx, "Failed to close remote site", "error", err) } return case trace.IsConnectionProblem(err): - s.logger.Debug("Remote cluster is offline.") + s.logger.DebugContext(s.ctx, "Remote cluster is offline") default: - s.logger.Warnf("Could not perform cert authorities update: %v.", trace.DebugReport(err)) + s.logger.WarnContext(s.ctx, "Could not perform cert authorities update", "error", err) } } startedWaiting := s.clock.Now() select { case t := <-retry.After(): - s.logger.Debugf("Initiating new cert authority watch after waiting %v.", t.Sub(startedWaiting)) + s.logger.DebugContext(s.ctx, "Initiating new cert authority watch after applying backoff", "backoff_duration", t.Sub(startedWaiting)) retry.Inc() case <-s.ctx.Done(): return @@ -592,7 +593,7 @@ func (s *remoteSite) watchCertAuthorities(remoteWatcher *services.CertAuthorityW } defer func() { if err := localWatch.Close(); err != nil { - s.logger.WithError(err).Warn("Failed to close local ca watcher subscription.") + s.logger.WarnContext(s.ctx, "Failed to close local ca watcher subscription", "error", err) } }() @@ -607,7 +608,7 @@ func (s *remoteSite) watchCertAuthorities(remoteWatcher *services.CertAuthorityW } defer func() { if err := remoteWatch.Close(); err != nil { - s.logger.WithError(err).Warn("Failed to close remote ca watcher subscription.") + s.logger.WarnContext(s.ctx, "Failed to close remote ca watcher subscription", "error", err) } }() @@ -624,7 +625,7 @@ func (s *remoteSite) watchCertAuthorities(remoteWatcher *services.CertAuthorityW if err := s.remoteClient.RotateExternalCertAuthority(s.ctx, ca); err != nil { return trace.Wrap(err, "failed to push local cert authority") } - s.logger.Debugf("Pushed local cert authority %v", caID.String()) + s.logger.DebugContext(s.ctx, "Pushed local cert authority", "cert_authority", logutils.StringerAttr(caID)) localCAs[caType] = ca } @@ -650,7 +651,7 @@ func (s *remoteSite) watchCertAuthorities(remoteWatcher *services.CertAuthorityW // if CA is changed or does not exist, update backend if err != nil || !services.CertAuthoritiesEquivalent(oldRemoteCA, remoteCA) { - s.logger.Debugf("Ingesting remote cert authority %v", remoteCA.GetID()) + s.logger.DebugContext(s.ctx, "Ingesting remote cert authority", "cert_authority", logutils.StringerAttr(remoteCA.GetID())) if err := s.localClient.UpsertCertAuthority(s.ctx, remoteCA); err != nil { return trace.Wrap(err) } @@ -668,17 +669,16 @@ func (s *remoteSite) watchCertAuthorities(remoteWatcher *services.CertAuthorityW return trace.Wrap(err) } - s.logger.Debugf("Watching for cert authority changes.") + s.logger.DebugContext(s.ctx, "Watching for cert authority changes") for { select { case <-s.ctx.Done(): - s.logger.WithError(s.ctx.Err()).Debug("Context is closing.") return trace.Wrap(s.ctx.Err()) case <-localWatch.Done(): - s.logger.Warn("Local CertAuthority watcher subscription has closed") + s.logger.WarnContext(s.ctx, "Local CertAuthority watcher subscription has closed") return fmt.Errorf("local ca watcher for cluster %s has closed", s.srv.ClusterName) case <-remoteWatch.Done(): - s.logger.Warn("Remote CertAuthority watcher subscription has closed") + s.logger.WarnContext(s.ctx, "Remote CertAuthority watcher subscription has closed") return fmt.Errorf("remote ca watcher for cluster %s has closed", s.domainName) case evt := <-localWatch.Events(): switch evt.Type { @@ -699,7 +699,7 @@ func (s *remoteSite) watchCertAuthorities(remoteWatcher *services.CertAuthorityW // TODO(espadolini): figure out who should be responsible for validating the CA *once* newCA = newCA.Clone() if err := s.remoteClient.RotateExternalCertAuthority(s.ctx, newCA); err != nil { - log.WithError(err).Warn("Failed to rotate external ca") + s.logger.WarnContext(s.ctx, "Failed to rotate external ca", "error", err) return trace.Wrap(err) } @@ -724,13 +724,13 @@ func (s *remoteSite) watchCertAuthorities(remoteWatcher *services.CertAuthorityW } func (s *remoteSite) updateLocks(retry retryutils.Retry) { - s.logger.Debugf("Watching for remote lock changes.") + s.logger.DebugContext(s.ctx, "Watching for remote lock changes") for { startedWaiting := s.clock.Now() select { case t := <-retry.After(): - s.logger.Debugf("Initiating new lock watch after waiting %v.", t.Sub(startedWaiting)) + s.logger.DebugContext(s.ctx, "Initiating new lock watch after applying backoff", "backoff_duration", t.Sub(startedWaiting)) retry.Inc() case <-s.ctx.Done(): return @@ -739,11 +739,11 @@ func (s *remoteSite) updateLocks(retry retryutils.Retry) { if err := s.watchLocks(); err != nil { switch { case trace.IsNotImplemented(err): - s.logger.Debugf("Remote cluster %v does not support locks yet.", s.domainName) + s.logger.DebugContext(s.ctx, "Remote cluster does not support locks yet", "cluster", s.domainName) case trace.IsConnectionProblem(err): - s.logger.Debugf("Remote cluster %v is offline.", s.domainName) + s.logger.DebugContext(s.ctx, "Remote cluster is offline", "cluster", s.domainName) default: - s.logger.WithError(err).Warn("Could not update remote locks.") + s.logger.WarnContext(s.ctx, "Could not update remote locks", "error", err) } } } @@ -752,22 +752,21 @@ func (s *remoteSite) updateLocks(retry retryutils.Retry) { func (s *remoteSite) watchLocks() error { watcher, err := s.srv.LockWatcher.Subscribe(s.ctx) if err != nil { - s.logger.WithError(err).Error("Failed to subscribe to LockWatcher") + s.logger.ErrorContext(s.ctx, "Failed to subscribe to LockWatcher", "error", err) return err } defer func() { if err := watcher.Close(); err != nil { - s.logger.WithError(err).Warn("Failed to close lock watcher subscription.") + s.logger.WarnContext(s.ctx, "Failed to close lock watcher subscription", "error", err) } }() for { select { case <-watcher.Done(): - s.logger.WithError(watcher.Error()).Warn("Lock watcher subscription has closed") + s.logger.WarnContext(s.ctx, "Lock watcher subscription has closed", "error", watcher.Error()) return trace.Wrap(watcher.Error()) case <-s.ctx.Done(): - s.logger.WithError(s.ctx.Err()).Debug("Context is closing.") return trace.Wrap(s.ctx.Err()) case evt := <-watcher.Events(): switch evt.Type { @@ -822,7 +821,10 @@ func (s *remoteSite) Dial(params reversetunnelclient.DialParams) (net.Conn, erro } func (s *remoteSite) DialTCP(params reversetunnelclient.DialParams) (net.Conn, error) { - s.logger.Debugf("Dialing from %v to %v.", params.From, params.To) + s.logger.DebugContext(s.ctx, "Initiating dial request", + "source_addr", logutils.StringerAttr(params.From), + "target_addr", logutils.StringerAttr(params.To), + ) conn, err := s.connThroughTunnel(&sshutils.DialReq{ Address: params.To.String(), @@ -843,7 +845,10 @@ func (s *remoteSite) dialAndForward(params reversetunnelclient.DialParams) (_ ne if params.GetUserAgent == nil && !params.IsAgentlessNode { return nil, trace.BadParameter("user agent getter is required for teleport nodes") } - s.logger.Debugf("Dialing and forwarding from %v to %v.", params.From, params.To) + s.logger.DebugContext(s.ctx, "Initiating dial and forward request", + "source_addr", logutils.StringerAttr(params.From), + "target_addr", logutils.StringerAttr(params.To), + ) // request user agent connection if a SSH user agent is set var userAgent teleagent.Agent @@ -930,7 +935,7 @@ func (s *remoteSite) dialAndForward(params reversetunnelclient.DialParams) (_ ne // UseTunnel makes a channel request asking for the type of connection. If // the other side does not respond (older cluster) or takes to long to // respond, be on the safe side and assume it's not a tunnel connection. -func UseTunnel(logger *log.Entry, c *sshutils.ChConn) bool { +func UseTunnel(logger *slog.Logger, c *sshutils.ChConn) bool { responseCh := make(chan bool, 1) go func() { @@ -946,13 +951,16 @@ func UseTunnel(logger *log.Entry, c *sshutils.ChConn) bool { case response := <-responseCh: return response case <-time.After(1 * time.Second): - logger.Debugf("Timed out waiting for response: returning false.") + logger.DebugContext(context.Background(), "Timed out waiting for response: returning false") return false } } func (s *remoteSite) connThroughTunnel(req *sshutils.DialReq) (*sshutils.ChConn, error) { - s.logger.Debugf("Requesting connection to %v [%v] in remote cluster.", req.Address, req.ServerID) + s.logger.DebugContext(s.ctx, "Requesting connection in remote cluster.", + "target_address", req.Address, + "target_server_id", req.ServerID, + ) // Loop through existing remote connections and try and establish a // connection over the "reverse tunnel". @@ -963,7 +971,7 @@ func (s *remoteSite) connThroughTunnel(req *sshutils.DialReq) (*sshutils.ChConn, if err == nil { return conn, nil } - s.logger.WithError(err).Warn("Request for connection to remote site failed") + s.logger.WarnContext(s.ctx, "Request for connection to remote site failed", "error", err) } // Didn't connect and no error? This means we didn't have any connected diff --git a/lib/reversetunnel/srv.go b/lib/reversetunnel/srv.go index 10591e2042bdd..eb7483eec6477 100644 --- a/lib/reversetunnel/srv.go +++ b/lib/reversetunnel/srv.go @@ -31,7 +31,6 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" @@ -55,6 +54,7 @@ import ( "github.com/gravitational/teleport/lib/sshca" "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/utils" + logutils "github.com/gravitational/teleport/lib/utils/log" ) var ( @@ -111,8 +111,8 @@ type server struct { // ctx is a context used for signaling and broadcast ctx context.Context - // log specifies the logger - log log.FieldLogger + // logger specifies the logger + logger *slog.Logger // proxyWatcher monitors changes to the proxies // and broadcasts updates @@ -186,10 +186,6 @@ type Config struct { // Component is a component used in logs Component string - // Log specifies the logger - // TODO(tross): remove this once Logger is used everywhere - Log log.FieldLogger - // Logger specifies the logger Logger *slog.Logger @@ -265,10 +261,6 @@ func (cfg *Config) CheckAndSetDefaults() error { if cfg.Component == "" { cfg.Component = teleport.Component(teleport.ComponentProxy, teleport.ComponentServer) } - if cfg.Log == nil { - cfg.Log = log.StandardLogger() - } - cfg.Log = cfg.Log.WithField(teleport.ComponentKey, cfg.Component) if cfg.Logger == nil { cfg.Logger = slog.Default() @@ -331,7 +323,7 @@ func NewServer(cfg Config) (reversetunnelclient.Server, error) { cancel: cancel, proxyWatcher: proxyWatcher, clusterPeers: make(map[string]*clusterPeers), - log: cfg.Log, + logger: cfg.Logger, offlineThreshold: offlineThreshold, proxySigner: cfg.PROXYSigner, } @@ -384,9 +376,9 @@ func remoteClustersMap(rc []types.RemoteCluster) map[string]types.RemoteCluster func (s *server) disconnectClusters(connectedRemoteClusters []*remoteSite, remoteMap map[string]types.RemoteCluster) error { for _, cluster := range connectedRemoteClusters { if _, ok := remoteMap[cluster.GetName()]; !ok { - s.log.Infof("Remote cluster %q has been deleted. Disconnecting it from the proxy.", cluster.GetName()) + s.logger.InfoContext(s.ctx, "Remote cluster has been deleted, disconnecting it from the proxy", "remote_cluster", cluster.GetName()) if err := s.onSiteTunnelClose(&alwaysClose{RemoteSite: cluster}); err != nil { - s.log.Debugf("Failure closing cluster %q: %v.", cluster.GetName(), err) + s.logger.DebugContext(s.ctx, "Failure closing cluster", "remote_cluster", cluster.GetName(), "error", err) } remoteClustersStats.DeleteLabelValues(cluster.GetName()) } @@ -399,36 +391,36 @@ func (s *server) periodicFunctions() { defer ticker.Stop() if err := s.fetchClusterPeers(); err != nil { - s.log.Warningf("Failed to fetch cluster peers: %v.", err) + s.logger.WarnContext(s.Context, "Failed to fetch cluster peers", "error", err) } for { select { case <-s.ctx.Done(): - s.log.Debugf("Closing.") + s.logger.DebugContext(s.ctx, "Closing") return // Proxies have been updated, notify connected agents about the update. case proxies := <-s.proxyWatcher.ResourcesC: s.fanOutProxies(proxies) case <-ticker.C: if err := s.fetchClusterPeers(); err != nil { - s.log.WithError(err).Warn("Failed to fetch cluster peers") + s.logger.WarnContext(s.ctx, "Failed to fetch cluster peers", "error", err) } connectedRemoteClusters := s.getRemoteClusters() remoteClusters, err := s.localAccessPoint.GetRemoteClusters(s.ctx) if err != nil { - s.log.WithError(err).Warn("Failed to get remote clusters") + s.logger.WarnContext(s.ctx, "Failed to get remote clusters", "error", err) } remoteMap := remoteClustersMap(remoteClusters) if err := s.disconnectClusters(connectedRemoteClusters, remoteMap); err != nil { - s.log.Warningf("Failed to disconnect clusters: %v.", err) + s.logger.WarnContext(s.ctx, "Failed to disconnect clusters", "error", err) } if err := s.reportClusterStats(connectedRemoteClusters, remoteMap); err != nil { - s.log.Warningf("Failed to report cluster stats: %v.", err) + s.logger.WarnContext(s.ctx, "Failed to report cluster stats", "error", err) } } } @@ -555,11 +547,11 @@ func (s *server) removeClusterPeers(conns []types.TunnelConnection) { for _, conn := range conns { peers, ok := s.clusterPeers[conn.GetClusterName()] if !ok { - s.log.Warningf("failed to remove cluster peer, not found peers for %v.", conn) + s.logger.WarnContext(s.ctx, "failed to remove missing cluster peer", "tunnel_connection", logutils.StringerAttr(conn)) continue } peers.removePeer(conn) - s.log.Debugf("Removed cluster peer %v.", conn) + s.logger.DebugContext(s.ctx, "Removed cluster peer", "tunnel_connection", logutils.StringerAttr(conn)) } } @@ -620,11 +612,11 @@ func (s *server) DrainConnections(ctx context.Context) error { // Ensure listener is closed before sending reconnects. err := s.srv.Close() s.RLock() - s.log.Debugf("Advising reconnect to local site: %s", s.localSite.GetName()) + s.logger.DebugContext(ctx, "Advising reconnect to local site", "local_site", s.localSite.GetName()) go s.localSite.adviseReconnect(ctx) for _, site := range s.remoteSites { - s.log.Debugf("Advising reconnect to remote site: %s", site.GetName()) + s.logger.DebugContext(ctx, "Advising reconnect to remote site", "remote_site", site.GetName()) go site.adviseReconnect(ctx) } s.RUnlock() @@ -650,7 +642,7 @@ func (s *server) HandleNewChan(ctx context.Context, ccx *sshutils.ConnectionCont switch channelType { // Heartbeats can come from nodes or proxies. case chanHeartbeat: - s.handleHeartbeat(conn, sconn, nch) + s.handleHeartbeat(ctx, conn, sconn, nch) // Transport requests come from nodes requesting a connection to the Auth // Server through the reverse tunnel. case constants.ChanTransport: @@ -665,19 +657,20 @@ func (s *server) HandleNewChan(ctx context.Context, ccx *sshutils.ConnectionCont if channelType == "session" { msg = "Cannot open new SSH session on reverse tunnel. Are you connecting to the right port?" } - s.log.Warn(msg) + //nolint:sloglint // message should be a constant but in this case we are creating it at runtime. + s.logger.WarnContext(ctx, msg) s.rejectRequest(nch, ssh.ConnectionFailed, msg) return } } func (s *server) handleTransport(sconn *ssh.ServerConn, nch ssh.NewChannel) { - s.log.Debug("Received transport request.") + s.logger.DebugContext(s.ctx, "Received transport request") channel, requestC, err := nch.Accept() if err != nil { sconn.Close() // avoid WithError to reduce log spam on network errors - s.log.Warnf("Failed to accept request: %v.", err) + s.logger.WarnContext(s.ctx, "Failed to accept request", "error", err) return } @@ -696,7 +689,7 @@ func (s *server) handleTransportChannel(sconn *ssh.ServerConn, ch ssh.Channel, r return case <-time.After(apidefaults.DefaultIOTimeout): go ssh.DiscardRequests(reqC) - s.log.Warn("Timed out waiting for transport dial request.") + s.logger.WarnContext(s.ctx, "Timed out waiting for transport dial request") return case r, ok := <-reqC: if !ok { @@ -708,13 +701,12 @@ func (s *server) handleTransportChannel(sconn *ssh.ServerConn, ch ssh.Channel, r dialReq := parseDialReq(req.Payload) if dialReq.Address != constants.RemoteAuthServer { - s.log.WithField("address", dialReq.Address). - Warn("Received dial request for unexpected address, routing to the auth server anyway.") + s.logger.WarnContext(s.ctx, "Received dial request for unexpected address, routing to the auth server anyway", "address", dialReq.Address) } authAddress := utils.ChooseRandomString(s.LocalAuthAddresses) if authAddress == "" { - s.log.Error("No auth servers configured.") + s.logger.ErrorContext(s.ctx, "No auth servers configured") fmt.Fprint(ch.Stderr(), "internal server error") req.Reply(false, nil) return @@ -726,7 +718,7 @@ func (s *server) handleTransportChannel(sconn *ssh.ServerConn, ch ssh.Channel, r if s.proxySigner != nil && clientSrcAddr != nil && clientDstAddr != nil { h, err := s.proxySigner.SignPROXYHeader(clientSrcAddr, clientDstAddr) if err != nil { - s.log.WithError(err).Error("Failed to create signed PROXY header.") + s.logger.ErrorContext(s.ctx, "Failed to create signed PROXY header", "error", err) fmt.Fprint(ch.Stderr(), "internal server error") req.Reply(false, nil) } @@ -736,7 +728,7 @@ func (s *server) handleTransportChannel(sconn *ssh.ServerConn, ch ssh.Channel, r d := net.Dialer{Timeout: apidefaults.DefaultIOTimeout} conn, err := d.DialContext(s.ctx, "tcp", authAddress) if err != nil { - s.log.Errorf("Failed to dial auth: %v.", err) + s.logger.ErrorContext(s.ctx, "Failed to dial auth", "error", err) fmt.Fprint(ch.Stderr(), "failed to dial auth server") req.Reply(false, nil) return @@ -745,7 +737,7 @@ func (s *server) handleTransportChannel(sconn *ssh.ServerConn, ch ssh.Channel, r _ = conn.SetWriteDeadline(time.Now().Add(apidefaults.DefaultIOTimeout)) if _, err := conn.Write(proxyHeader); err != nil { - s.log.Errorf("Failed to send PROXY header: %v.", err) + s.logger.ErrorContext(s.ctx, "Failed to send PROXY header", "error", err) fmt.Fprint(ch.Stderr(), "failed to dial auth server") req.Reply(false, nil) return @@ -753,7 +745,7 @@ func (s *server) handleTransportChannel(sconn *ssh.ServerConn, ch ssh.Channel, r _ = conn.SetWriteDeadline(time.Time{}) if err := req.Reply(true, nil); err != nil { - s.log.Errorf("Failed to respond to dial request: %v.", err) + s.logger.ErrorContext(s.ctx, "Failed to respond to dial request", "error", err) return } @@ -761,10 +753,10 @@ func (s *server) handleTransportChannel(sconn *ssh.ServerConn, ch ssh.Channel, r } // TODO(awly): unit test this -func (s *server) handleHeartbeat(conn net.Conn, sconn *ssh.ServerConn, nch ssh.NewChannel) { - s.log.Debugf("New tunnel from %v.", sconn.RemoteAddr()) +func (s *server) handleHeartbeat(ctx context.Context, conn net.Conn, sconn *ssh.ServerConn, nch ssh.NewChannel) { + s.logger.DebugContext(ctx, "New tunnel established", "remote_addr", logutils.StringerAttr(sconn.RemoteAddr())) if sconn.Permissions.Extensions[utils.ExtIntCertType] != utils.ExtIntCertTypeHost { - s.log.Error(trace.BadParameter("can't retrieve certificate type in certType")) + s.logger.ErrorContext(ctx, "can't retrieve certificate type in certtype@teleport extension") return } @@ -772,7 +764,7 @@ func (s *server) handleHeartbeat(conn net.Conn, sconn *ssh.ServerConn, nch ssh.N // nodes it's a node dialing back. val, ok := sconn.Permissions.Extensions[extCertRole] if !ok { - s.log.Errorf("Failed to accept connection, missing %q extension", extCertRole) + s.logger.ErrorContext(ctx, "Failed to accept connection, missing role extension") s.rejectRequest(nch, ssh.ConnectionFailed, "unknown role") return } @@ -781,64 +773,64 @@ func (s *server) handleHeartbeat(conn net.Conn, sconn *ssh.ServerConn, nch ssh.N switch role { // Node is dialing back. case types.RoleNode: - s.handleNewService(role, conn, sconn, nch, types.NodeTunnel) + s.handleNewService(ctx, role, conn, sconn, nch, types.NodeTunnel) // App is dialing back. case types.RoleApp: - s.handleNewService(role, conn, sconn, nch, types.AppTunnel) + s.handleNewService(ctx, role, conn, sconn, nch, types.AppTunnel) // Kubernetes service is dialing back. case types.RoleKube: - s.handleNewService(role, conn, sconn, nch, types.KubeTunnel) + s.handleNewService(ctx, role, conn, sconn, nch, types.KubeTunnel) // Database proxy is dialing back. case types.RoleDatabase: - s.handleNewService(role, conn, sconn, nch, types.DatabaseTunnel) + s.handleNewService(ctx, role, conn, sconn, nch, types.DatabaseTunnel) // Proxy is dialing back. case types.RoleProxy: - s.handleNewCluster(conn, sconn, nch) + s.handleNewCluster(ctx, conn, sconn, nch) case types.RoleWindowsDesktop: - s.handleNewService(role, conn, sconn, nch, types.WindowsDesktopTunnel) + s.handleNewService(ctx, role, conn, sconn, nch, types.WindowsDesktopTunnel) case types.RoleOkta: - s.handleNewService(role, conn, sconn, nch, types.OktaTunnel) + s.handleNewService(ctx, role, conn, sconn, nch, types.OktaTunnel) // Unknown role. default: - s.log.Errorf("Unsupported role attempting to connect: %v", val) + s.logger.ErrorContext(ctx, "Unsupported role attempting to connect", "role", val) s.rejectRequest(nch, ssh.ConnectionFailed, fmt.Sprintf("unsupported role %v", val)) } } -func (s *server) handleNewService(role types.SystemRole, conn net.Conn, sconn *ssh.ServerConn, nch ssh.NewChannel, connType types.TunnelType) { +func (s *server) handleNewService(ctx context.Context, role types.SystemRole, conn net.Conn, sconn *ssh.ServerConn, nch ssh.NewChannel, connType types.TunnelType) { cluster, rconn, err := s.upsertServiceConn(conn, sconn, connType) if err != nil { - s.log.Errorf("Failed to upsert %s: %v.", role, err) + s.logger.ErrorContext(ctx, "Failed to upsert service connection", "role", role, "error", err) sconn.Close() return } ch, req, err := nch.Accept() if err != nil { - s.log.Errorf("Failed to accept on channel: %v.", err) + s.logger.ErrorContext(ctx, "Failed to accept on channel", "error", err) sconn.Close() return } - go cluster.handleHeartbeat(rconn, ch, req) + go cluster.handleHeartbeat(ctx, rconn, ch, req) } -func (s *server) handleNewCluster(conn net.Conn, sshConn *ssh.ServerConn, nch ssh.NewChannel) { +func (s *server) handleNewCluster(ctx context.Context, conn net.Conn, sshConn *ssh.ServerConn, nch ssh.NewChannel) { // add the incoming site (cluster) to the list of active connections: site, remoteConn, err := s.upsertRemoteCluster(conn, sshConn) if err != nil { - s.log.Error(trace.Wrap(err)) + s.logger.ErrorContext(ctx, "failed to upsert remote cluster connection", "error", err) s.rejectRequest(nch, ssh.ConnectionFailed, "failed to accept incoming cluster connection") return } // accept the request and start the heartbeat on it: ch, req, err := nch.Accept() if err != nil { - s.log.Error(trace.Wrap(err)) + s.logger.ErrorContext(ctx, "Failed to accept on channel", "error", err) sshConn.Close() return } - go site.handleHeartbeat(remoteConn, ch, req) + go site.handleHeartbeat(ctx, remoteConn, ch, req) } func (s *server) requireLocalAgentForConn(sconn *ssh.ServerConn, connType types.TunnelType) error { @@ -864,15 +856,15 @@ func (s *server) getTrustedCAKeysByID(id types.CertAuthID) ([]ssh.PublicKey, err } func (s *server) keyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (perm *ssh.Permissions, err error) { - logger := s.log.WithFields(log.Fields{ - "remote": conn.RemoteAddr(), - "user": conn.User(), - }) + logger := s.logger.With( + "remote_addr", logutils.StringerAttr(conn.RemoteAddr()), + "user", conn.User(), + ) // The crypto/x/ssh package won't log the returned error for us, do it // manually. defer func() { if err != nil { - logger.Warnf("Failed to authenticate client, err: %v.", err) + logger.WarnContext(s.ctx, "Failed to authenticate client", "error", err) } }() @@ -920,7 +912,7 @@ func (s *server) keyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (perm *ssh.Pe return nil, trace.BadParameter("unsupported cert type: %v.", cert.CertType) } - if err := s.checkClientCert(logger, conn.User(), clusterName, cert, caType); err != nil { + if err := s.checkClientCert(conn.User(), clusterName, cert, caType); err != nil { return nil, trace.Wrap(err) } return &ssh.Permissions{ @@ -935,7 +927,7 @@ func (s *server) keyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (perm *ssh.Pe // checkClientCert verifies that client certificate is signed by the recognized // certificate authority. -func (s *server) checkClientCert(logger *log.Entry, user string, clusterName string, cert *ssh.Certificate, caType types.CertAuthType) error { +func (s *server) checkClientCert(user string, clusterName string, cert *ssh.Certificate, caType types.CertAuthType) error { // fetch keys of the certificate authority to check // if there is a match keys, err := s.getTrustedCAKeysByID(types.CertAuthID{ @@ -1024,7 +1016,10 @@ func (s *server) upsertRemoteCluster(conn net.Conn, sshConn *ssh.ServerConn) (*r } s.remoteSites = append(s.remoteSites, site) } - site.logger.Infof("Connection <- %v, clusters: %d.", conn.RemoteAddr(), len(s.remoteSites)) + site.logger.InfoContext(s.ctx, "Processed inbound connection from remote cluster", + "source_addr", logutils.StringerAttr(conn.RemoteAddr()), + "tunnel_count", len(s.remoteSites), + ) // treat first connection as a registered heartbeat, // otherwise the connection information will appear after initial // heartbeat delay @@ -1146,7 +1141,7 @@ func (s *server) fanOutProxies(proxies []types.Server) { func (s *server) rejectRequest(ch ssh.NewChannel, reason ssh.RejectionReason, msg string) { if err := ch.Reject(reason, msg); err != nil { - s.log.Warnf("Failed rejecting new channel request: %v", err) + s.logger.WarnContext(s.ctx, "Failed rejecting new channel request", "error", err) } } @@ -1182,12 +1177,10 @@ func newRemoteSite(srv *server, domainName string, sconn ssh.Conn) (*remoteSite, srv: srv, domainName: domainName, connInfo: connInfo, - logger: log.WithFields(log.Fields{ - teleport.ComponentKey: teleport.ComponentReverseTunnelServer, - teleport.ComponentFields: log.Fields{ - "cluster": domainName, - }, - }), + logger: slog.With( + teleport.ComponentKey, teleport.ComponentReverseTunnelServer, + "cluster", domainName, + ), ctx: closeContext, cancel: cancel, clock: srv.Clock, diff --git a/lib/reversetunnel/srv_test.go b/lib/reversetunnel/srv_test.go index 327b194ac6ae7..2477739df359a 100644 --- a/lib/reversetunnel/srv_test.go +++ b/lib/reversetunnel/srv_test.go @@ -29,7 +29,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh" @@ -64,7 +63,7 @@ func TestServerKeyAuth(t *testing.T) { require.NoError(t, err) s := &server{ - log: utils.NewLoggerForTests(), + logger: utils.NewSlogLoggerForTests(), Config: Config{Clock: clockwork.NewRealClock()}, localAccessPoint: mockAccessPoint{ ca: ca, @@ -204,8 +203,8 @@ func TestOnlyAuthDial(t *testing.T) { badListenerAddr := acceptAndCloseListener(t, true) srv := &server{ - log: logrus.StandardLogger(), - ctx: ctx, + logger: utils.NewSlogLoggerForTests(), + ctx: ctx, Config: Config{ LocalAuthAddresses: []string{goodListenerAddr}, }, diff --git a/lib/reversetunnel/transport.go b/lib/reversetunnel/transport.go index 2f3d5cfc697f4..b3d338e9bb62b 100644 --- a/lib/reversetunnel/transport.go +++ b/lib/reversetunnel/transport.go @@ -23,12 +23,12 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net" "net/netip" "time" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" @@ -61,7 +61,7 @@ func parseDialReq(payload []byte) *sshutils.DialReq { // transport is used to build a connection to the target host. type transport struct { component string - log logrus.FieldLogger + logger *slog.Logger closeContext context.Context authClient authclient.ProxyAccessPoint authServers []string @@ -126,7 +126,7 @@ func (p *transport) start() { return } case <-time.After(apidefaults.DefaultIOTimeout): - p.log.Warnf("Transport request failed: timed out waiting for request.") + p.logger.WarnContext(p.closeContext, "Transport request failed: timed out waiting for request") return } @@ -140,8 +140,12 @@ func (p *transport) start() { if !p.forwardClientAddress { // This shouldn't happen in normal operation. Either malicious user or misconfigured client. if dreq.ClientSrcAddr != "" || dreq.ClientDstAddr != "" { - p.log.Warnf("Received unexpected dial request with client source address %q, "+ - "client destination address %q, when they should be empty.", dreq.ClientSrcAddr, dreq.ClientDstAddr) + const msg = "Received unexpected dial request with client source address and " + + "client destination address populated, when they should be empty." + p.logger.WarnContext(p.closeContext, msg, + "client_src_addr", dreq.ClientSrcAddr, + "client_dest_addr", dreq.ClientDstAddr, + ) } // Make sure address fields are overwritten. @@ -154,7 +158,11 @@ func (p *transport) start() { } } - p.log.Debugf("Received out-of-band proxy transport request for %v [%v], from %v.", dreq.Address, dreq.ServerID, dreq.ClientSrcAddr) + p.logger.DebugContext(p.closeContext, "Received out-of-band proxy transport request", + "target_address", dreq.Address, + "taget_server_id", dreq.ServerID, + "client_addr", dreq.ClientSrcAddr, + ) // directAddress will hold the address of the node to dial to, if we don't // have a tunnel for it. @@ -165,7 +173,7 @@ func (p *transport) start() { // Connect to an Auth Server. case reversetunnelclient.RemoteAuthServer: if len(p.authServers) == 0 { - p.log.Errorf("connection rejected: no auth servers configured") + p.logger.ErrorContext(p.closeContext, "connection rejected: no auth servers configured") p.reply(req, false, []byte("no auth servers configured")) return @@ -190,11 +198,14 @@ func (p *transport) start() { return } if err := req.Reply(true, []byte("Connected.")); err != nil { - p.log.Errorf("Failed responding OK to %q request: %v", req.Type, err) + p.logger.ErrorContext(p.closeContext, "Failed responding OK to request", + "request_type", req.Type, + "error", err, + ) return } - p.log.Debug("Handing off connection to a local kubernetes service") + p.logger.DebugContext(p.closeContext, "Handing off connection to a local kubernetes service") // If dreq has ClientSrcAddr we wrap connection var clientConn net.Conn = sshutils.NewChConn(p.sconn, p.channel) @@ -211,7 +222,7 @@ func (p *transport) start() { p.reply(req, false, []byte("connection rejected: configure kubernetes proxy for this cluster.")) return } - p.log.Debugf("Forwarding connection to %q", p.kubeDialAddr.Addr) + p.logger.DebugContext(p.closeContext, "Forwarding connection to kubernetes proxy", "kube_proxy_addr", p.kubeDialAddr.Addr) directAddress = p.kubeDialAddr.Addr } @@ -227,17 +238,20 @@ func (p *transport) start() { if p.server != nil { if p.sconn == nil { - p.log.Debug("Connection rejected: server connection missing") + p.logger.DebugContext(p.closeContext, "Connection rejected: server connection missing") p.reply(req, false, []byte("connection rejected: server connection missing")) return } if err := req.Reply(true, []byte("Connected.")); err != nil { - p.log.Errorf("Failed responding OK to %q request: %v", req.Type, err) + p.logger.ErrorContext(p.closeContext, "Failed responding OK to request", + "request_type", req.Type, + "error", err, + ) return } - p.log.Debugf("Handing off connection to a local %q service.", dreq.ConnType) + p.logger.DebugContext(p.closeContext, "Handing off connection to a local service.", "conn_type", dreq.ConnType) // If dreq has ClientSrcAddr we wrap connection var clientConn net.Conn = sshutils.NewChConn(p.sconn, p.channel) @@ -294,13 +308,19 @@ func (p *transport) start() { // Dial was successful. if err := req.Reply(true, []byte("Connected.")); err != nil { - p.log.Errorf("Failed responding OK to %q request: %v", req.Type, err) + p.logger.ErrorContext(p.closeContext, "Failed responding OK to request", + "request_type", req.Type, + "error", err, + ) if err := conn.Close(); err != nil { - p.log.Errorf("Failed closing connection: %v", err) + p.logger.ErrorContext(p.closeContext, "Failed closing connection", "error", err) } return } - p.log.Debugf("Successfully dialed to %v %q, start proxying.", dreq.Address, dreq.ServerID) + p.logger.DebugContext(p.closeContext, "Successfully dialed to target, starting to proxy", + "target_addr", dreq.Address, + "target_server_id", dreq.ServerID, + ) // Start processing channel requests. Pass in a context that wraps the passed // in context with a context that closes when this function returns to @@ -314,9 +334,9 @@ func (p *transport) start() { if len(signedHeader) > 0 { _, err = conn.Write(signedHeader) if err != nil { - p.log.Errorf("Could not write PROXY header to the connection: %v", err) + p.logger.ErrorContext(p.closeContext, "Could not write PROXY header to the connection", "error", err) if err := conn.Close(); err != nil { - p.log.Errorf("Failed closing connection: %v", err) + p.logger.ErrorContext(p.closeContext, "Failed closing connection", "error", err) } return } @@ -342,7 +362,7 @@ func (p *transport) start() { select { case <-errorCh: case <-p.closeContext.Done(): - p.log.Warnf("Proxy transport failed: closing context.") + p.logger.WarnContext(p.closeContext, "Proxy transport failed: closing context") return } } @@ -375,7 +395,7 @@ func (p *transport) handleChannelRequests(closeContext context.Context, useTunne func (p *transport) getConn(addr string, r *sshutils.DialReq) (net.Conn, bool, error) { // This function doesn't attempt to dial if a host with one of the // search names is not registered. It's a fast check. - p.log.Debugf("Attempting to dial through tunnel with server ID %q.", r.ServerID) + p.logger.DebugContext(p.closeContext, "Attempting to dial server through tunnel", "target_server_id", r.ServerID) conn, err := p.tunnelDial(r) if err != nil { if !trace.IsNotFound(err) { @@ -394,13 +414,13 @@ func (p *transport) getConn(addr string, r *sshutils.DialReq) (net.Conn, bool, e } errTun := err - p.log.Debugf("Attempting to dial directly %q.", addr) + p.logger.DebugContext(p.closeContext, "Attempting to dial server directly", "taget_addr", addr) conn, err = p.directDial(addr) if err != nil { return nil, false, trace.ConnectionProblem(err, "failed dialing through tunnel (%v) or directly (%v)", errTun, err) } - p.log.Debugf("Returning direct dialed connection to %q.", addr) + p.logger.DebugContext(p.closeContext, "Returning direct dialed connection", "target_addr", addr) // Requests to get a connection to the remote auth server do not provide a ConnType, // and since an empty ConnType is converted to [types.NodeTunnel] in CheckAndSetDefaults, @@ -413,7 +433,7 @@ func (p *transport) getConn(addr string, r *sshutils.DialReq) (net.Conn, bool, e return conn, false, nil } - p.log.Debugf("Returning connection dialed through tunnel with server ID %v.", r.ServerID) + p.logger.DebugContext(p.closeContext, "Returning connection to server dialed through tunnel", "target_server_id", r.ServerID) if r.ConnType == types.NodeTunnel { return proxy.NewProxiedMetricConn(conn), true, nil @@ -449,10 +469,10 @@ func (p *transport) tunnelDial(r *sshutils.DialReq) (net.Conn, error) { func (p *transport) reply(req *ssh.Request, ok bool, msg []byte) { if !ok { - p.log.Debugf("Non-ok reply to %q request: %s", req.Type, msg) + p.logger.DebugContext(p.closeContext, "Non-ok reply to request", "request_type", req.Type, "error", string(msg)) } if err := req.Reply(ok, msg); err != nil { - p.log.Warnf("Failed sending reply to %q request on SSH channel: %v", req.Type, err) + p.logger.WarnContext(p.closeContext, "Failed sending reply to request", "request_type", req.Type, "error", err) } } diff --git a/lib/reversetunnelclient/api_with_roles.go b/lib/reversetunnelclient/api_with_roles.go index 4b4eeff886871..ddeb7ff090f50 100644 --- a/lib/reversetunnelclient/api_with_roles.go +++ b/lib/reversetunnelclient/api_with_roles.go @@ -20,9 +20,9 @@ package reversetunnelclient import ( "context" + "log/slog" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/services" @@ -75,7 +75,7 @@ func (t *TunnelWithRoles) GetSites() ([]RemoteSite, error) { if !trace.IsNotFound(err) { return nil, trace.Wrap(err) } - logrus.Warningf("Skipping dangling cluster %q, no remote cluster resource found.", cluster.GetName()) + slog.WarnContext(ctx, "Skipping dangling cluster, no remote cluster resource found", "cluster", cluster.GetName()) continue } if err := t.accessChecker.CheckAccessToRemoteCluster(rc); err != nil { diff --git a/lib/service/service.go b/lib/service/service.go index fb2b70e19934c..39666c2aa1d91 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -4443,7 +4443,6 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { PollingPeriod: process.Config.PollingPeriod, FIPS: cfg.FIPS, Emitter: streamEmitter, - Log: process.log, Logger: process.logger, LockWatcher: lockWatcher, PeerClient: peerClient, @@ -5019,9 +5018,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { return nil }) - rcWatchLog := logrus.WithFields(logrus.Fields{ - teleport.ComponentKey: teleport.Component(teleport.ComponentReverseTunnelAgent, process.id), - }) + rcWatchLog := process.logger.With(teleport.ComponentKey, teleport.Component(teleport.ComponentReverseTunnelAgent, process.id)) // Create and register reverse tunnel AgentPool. rcWatcher, err := reversetunnel.NewRemoteClusterTunnelManager(reversetunnel.RemoteClusterTunnelManagerConfig{ @@ -5033,7 +5030,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { KubeDialAddr: utils.DialAddrFromListenAddr(kubeDialAddr(cfg.Proxy, clusterNetworkConfig.GetProxyListenerMode())), ReverseTunnelServer: tsrv, FIPS: process.Config.FIPS, - Log: rcWatchLog, + Logger: rcWatchLog, LocalAuthAddresses: utils.NetAddrsToStrings(process.Config.AuthServerAddresses()), PROXYSigner: proxySigner, }) @@ -5042,7 +5039,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { } process.RegisterCriticalFunc("proxy.reversetunnel.watcher", func() error { - rcWatchLog.Infof("Starting reverse tunnel agent pool.") + rcWatchLog.InfoContext(process.ExitContext(), "Starting reverse tunnel agent pool") done := make(chan struct{}) go func() { defer close(done) From 1a7466fdb41f18692d48c59ea717cc8f0f8b17d3 Mon Sep 17 00:00:00 2001 From: rosstimothy <39066650+rosstimothy@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:09:26 -0500 Subject: [PATCH 05/64] Complete lib/srv conversion to slog (#50124) * Convert lib/srv/server to use slog * Convert lib/srv/desktop to use slog * Add depguard rule to prevent logrus from sneaking back in * fix: authandler log using %v --- .golangci.yml | 43 ++++++++++++----------- lib/auth/windows/certificate_authority.go | 8 ++--- lib/srv/authhandlers.go | 2 +- lib/srv/desktop/rdp/rdpclient/client.go | 13 +++---- lib/srv/desktop/windows_server.go | 3 +- lib/srv/server/ec2_watcher.go | 4 +-- lib/srv/server/watcher.go | 4 +-- 7 files changed, 39 insertions(+), 38 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 2300891f36ec0..a28fd012d52c2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,54 +9,54 @@ issues: exclude-dirs-use-default: false exclude-rules: - linters: - - gosimple - text: "S1002: should omit comparison to bool constant" + - gosimple + text: 'S1002: should omit comparison to bool constant' - linters: - - revive - text: "exported: exported const" + - revive + text: 'exported: exported const' # TODO(hugoShaka): Remove once https://github.com/dominikh/go-tools/issues/1294 is fixed - linters: - - unused + - unused path: 'integrations/operator/controllers/resources/(.+)_controller_test\.go' # TODO(codingllama): Remove once we move to grpc.NewClient. - linters: [staticcheck] - text: "grpc.Dial is deprecated" + text: 'grpc.Dial is deprecated' - linters: [staticcheck] - text: "grpc.DialContext is deprecated" + text: 'grpc.DialContext is deprecated' # Deprecated gRPC dial options. Related to grpc.NewClient. - path: (client/client.go|client/proxy/client_test.go) # api/ linters: [staticcheck] # grpc.FailOnNonTempDialError # grpc.WithReturnConnectionError - text: "this DialOption is not supported by NewClient" + text: 'this DialOption is not supported by NewClient' - path: lib/kube/grpc/grpc_test.go linters: [staticcheck] - text: "grpc.WithBlock is deprecated" + text: 'grpc.WithBlock is deprecated' - path: lib/observability/tracing/client.go linters: [staticcheck] - text: "grpc.WithBlock is deprecated" + text: 'grpc.WithBlock is deprecated' - path: integrations/lib/config.go linters: [staticcheck] - text: "grpc.WithReturnConnectionError is deprecated" + text: 'grpc.WithReturnConnectionError is deprecated' - path: lib/service/service_test.go linters: [staticcheck] # grpc.WithReturnConnectionError # grpc.FailOnNonTempDialError - text: "this DialOption is not supported by NewClient" + text: 'this DialOption is not supported by NewClient' - path: integration/client_test.go linters: [staticcheck] - text: "grpc.WithReturnConnectionError is deprecated" + text: 'grpc.WithReturnConnectionError is deprecated' - path: integration/integration_test.go linters: [staticcheck] - text: "grpc.WithBlock is deprecated" + text: 'grpc.WithBlock is deprecated' - path: lib/multiplexer/multiplexer_test.go linters: [staticcheck] - text: "grpc.WithBlock is deprecated" + text: 'grpc.WithBlock is deprecated' - path: provider/provider.go # integrations/terraform linters: [staticcheck] - text: "grpc.WithReturnConnectionError is deprecated" + text: 'grpc.WithReturnConnectionError is deprecated' - linters: [govet] - text: "non-constant format string in call to github.com/gravitational/trace." + text: 'non-constant format string in call to github.com/gravitational/trace.' exclude-use-default: true max-same-issues: 0 max-issues-per-linter: 0 @@ -121,6 +121,7 @@ linters-settings: files: - '**/api/**' - '**/e/**' + - '**/lib/srv/**' deny: - pkg: github.com/sirupsen/logrus desc: 'use "log/slog" instead' @@ -130,7 +131,7 @@ linters-settings: client-tools: files: # Tests can do anything - - "!$test" + - '!$test' - '**/tool/tbot/**' - '**/lib/tbot/**' - '**/tool/tctl/**' @@ -158,7 +159,7 @@ linters-settings: cgo: files: # Tests can do anything - - "!$test" + - '!$test' - '**/tool/tbot/**' - '**/lib/client/**' - '!**/lib/integrations/**' @@ -240,8 +241,8 @@ linters-settings: require-specific: true revive: rules: - - name: unused-parameter - disabled: true + - name: unused-parameter + disabled: true sloglint: context: all key-naming-case: snake diff --git a/lib/auth/windows/certificate_authority.go b/lib/auth/windows/certificate_authority.go index 11c7049374be4..2c3a2789b3a51 100644 --- a/lib/auth/windows/certificate_authority.go +++ b/lib/auth/windows/certificate_authority.go @@ -20,9 +20,9 @@ package windows import ( "context" + "log/slog" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/authclient" @@ -45,7 +45,7 @@ type CertificateStoreConfig struct { // LDAPConfig is the ldap configuration LDAPConfig // Log is the logging sink for the service - Log logrus.FieldLogger + Logger *slog.Logger // ClusterName is the name of this cluster ClusterName string // LC is the LDAPClient @@ -114,9 +114,9 @@ func (c *CertificateStoreClient) updateCRL(ctx context.Context, crlDER []byte, c ); err != nil { return trace.Wrap(err) } - c.cfg.Log.Info("Updated CRL for Windows logins via LDAP") + c.cfg.Logger.InfoContext(ctx, "Updated CRL for Windows logins via LDAP") } else { - c.cfg.Log.Info("Added CRL for Windows logins via LDAP") + c.cfg.Logger.InfoContext(ctx, "Added CRL for Windows logins via LDAP") } return nil } diff --git a/lib/srv/authhandlers.go b/lib/srv/authhandlers.go index 03de6e26c779a..5d6d12ad05dff 100644 --- a/lib/srv/authhandlers.go +++ b/lib/srv/authhandlers.go @@ -633,7 +633,7 @@ func (a *ahLoginChecker) canLoginWithRBAC(cert *ssh.Certificate, ca types.CertAu // Use the server's shutdown context. ctx := a.c.Server.Context() - a.log.DebugContext(ctx, "Checking permissions for (%v,%v) to login to node with RBAC checks.", teleportUser, osUser) + a.log.DebugContext(ctx, "Checking permissions to login to node with RBAC checks", "teleport_user", teleportUser, "os_user", osUser) // get roles assigned to this user accessInfo, err := fetchAccessInfo(cert, ca, teleportUser, clusterName) diff --git a/lib/srv/desktop/rdp/rdpclient/client.go b/lib/srv/desktop/rdp/rdpclient/client.go index c7be6dd702016..821408d2208fa 100644 --- a/lib/srv/desktop/rdp/rdpclient/client.go +++ b/lib/srv/desktop/rdp/rdpclient/client.go @@ -73,6 +73,7 @@ import "C" import ( "context" "fmt" + "log/slog" "os" "runtime/cgo" "sync" @@ -81,7 +82,6 @@ import ( "unsafe" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/srv/desktop/tdp" @@ -98,14 +98,15 @@ func init() { // assume the user knows what they want) rl := os.Getenv("RUST_LOG") if rl == "" { - switch l := logrus.GetLevel(); l { - case logrus.TraceLevel: + ctx := context.Background() + switch { + case slog.Default().Enabled(ctx, logutils.TraceLevel): rustLogLevel = "trace" - case logrus.DebugLevel: + case slog.Default().Enabled(ctx, slog.LevelDebug): rustLogLevel = "debug" - case logrus.InfoLevel: + case slog.Default().Enabled(ctx, slog.LevelInfo): rustLogLevel = "info" - case logrus.WarnLevel: + case slog.Default().Enabled(ctx, slog.LevelWarn): rustLogLevel = "warn" default: rustLogLevel = "error" diff --git a/lib/srv/desktop/windows_server.go b/lib/srv/desktop/windows_server.go index 30e95a51d5840..8dbbad96b3fb6 100644 --- a/lib/srv/desktop/windows_server.go +++ b/lib/srv/desktop/windows_server.go @@ -36,7 +36,6 @@ import ( "github.com/go-ldap/ldap/v3" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport" apidefaults "github.com/gravitational/teleport/api/defaults" @@ -382,7 +381,7 @@ func NewWindowsService(cfg WindowsServiceConfig) (*WindowsService, error) { s.ca = windows.NewCertificateStoreClient(windows.CertificateStoreConfig{ AccessPoint: s.cfg.AccessPoint, LDAPConfig: caLDAPConfig, - Log: logrus.NewEntry(logrus.StandardLogger()), + Logger: slog.Default(), ClusterName: s.clusterName, LC: s.lc, }) diff --git a/lib/srv/server/ec2_watcher.go b/lib/srv/server/ec2_watcher.go index 79d20905408ac..cf3bb13a62367 100644 --- a/lib/srv/server/ec2_watcher.go +++ b/lib/srv/server/ec2_watcher.go @@ -20,6 +20,7 @@ package server import ( "context" + "log/slog" "sync" "time" @@ -27,7 +28,6 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" usageeventsv1 "github.com/gravitational/teleport/api/gen/proto/go/usageevents/v1" "github.com/gravitational/teleport/api/types" @@ -305,7 +305,7 @@ func newEC2InstanceFetcher(cfg ec2FetcherConfig) *ec2InstanceFetcher { }) } } else { - log.Debug("Not setting any tag filters as there is a '*:...' tag present and AWS doesnt allow globbing on keys") + slog.DebugContext(context.Background(), "Not setting any tag filters as there is a '*:...' tag present and AWS doesnt allow globbing on keys") } var parameters map[string]string if cfg.Matcher.Params == nil { diff --git a/lib/srv/server/watcher.go b/lib/srv/server/watcher.go index 8fa5de1ee9a90..436cd0f128cbc 100644 --- a/lib/srv/server/watcher.go +++ b/lib/srv/server/watcher.go @@ -20,10 +20,10 @@ package server import ( "context" + "log/slog" "time" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "github.com/gravitational/teleport/api/types" ) @@ -80,7 +80,7 @@ func (w *Watcher) sendInstancesOrLogError(instancesColl []Instances, err error) if trace.IsNotFound(err) { return } - log.WithError(err).Error("Failed to fetch instances") + slog.ErrorContext(context.Background(), "Failed to fetch instances", "error", err) return } for _, inst := range instancesColl { From 0b1e410755a3feb06d34da8f7e65bdd37b912582 Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Wed, 18 Dec 2024 08:34:01 -0800 Subject: [PATCH 06/64] Add check to installation script when we copy files from tarball (#50368) --- lib/web/scripts/node-join/install.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/web/scripts/node-join/install.sh b/lib/web/scripts/node-join/install.sh index 29ed2ee8cb73a..3d8403c00787d 100755 --- a/lib/web/scripts/node-join/install.sh +++ b/lib/web/scripts/node-join/install.sh @@ -840,7 +840,9 @@ install_from_file() { tar -xzf "${TEMP_DIR}/${DOWNLOAD_FILENAME}" -C "${TEMP_DIR}" # install binaries to /usr/local/bin for BINARY in ${TELEPORT_BINARY_LIST}; do - ${COPY_COMMAND} "${TELEPORT_ARCHIVE_PATH}/${BINARY}" "${TELEPORT_BINARY_DIR}/" + if [ -e "${TELEPORT_ARCHIVE_PATH}/${BINARY}" ]; then + ${COPY_COMMAND} "${TELEPORT_ARCHIVE_PATH}/${BINARY}" "${TELEPORT_BINARY_DIR}/" + fi done elif [[ ${TELEPORT_FORMAT} == "deb" ]]; then # convert teleport arch to deb arch From 4e02f1b52b391ca0dca5e0fb99a076400ba34975 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Wed, 18 Dec 2024 18:02:47 +0100 Subject: [PATCH 07/64] Show kube cluster name at the beginning of kube tabs (#50376) --- .../workspacesService/documentsService/documentsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts index 1edc82ed9c524..ad91d8081016c 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts @@ -425,7 +425,7 @@ export class DocumentsService { if (doc.kind === 'doc.gateway_kube') { const { params } = routing.parseKubeUri(doc.targetUri); this.update(doc.uri, { - title: [shellBinName, cwd, params.kubeId].filter(Boolean).join(' · '), + title: [params.kubeId, shellBinName].filter(Boolean).join(' · '), }); } } From b2dc107131778d9db69ad5b7bb3176817c722c8f Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Wed, 18 Dec 2024 10:07:05 -0800 Subject: [PATCH 08/64] Update e reference (#50391) --- e | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e b/e index ab7c9fca11b43..5ab219dde2a8d 160000 --- a/e +++ b/e @@ -1 +1 @@ -Subproject commit ab7c9fca11b43e3a3023fe4fd412b2d4e58377e6 +Subproject commit 5ab219dde2a8dbdac68823f084d94cb7c0223aeb From ef7a93fa3d4dda1ae0a65ae6e08aa5c3c1612547 Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Wed, 18 Dec 2024 14:05:56 -0600 Subject: [PATCH 09/64] Protobuf and configuration for Access Graph Azure Discovery (#50364) * Protobuf and configuration for Access Graph Azure Discovery * Adding godoc and removing Integration field from fileconf --- api/proto/teleport/legacy/types/types.proto | 12 +- api/types/discoveryconfig/derived.gen.go | 107 +- api/types/types.pb.go | 1122 +++++++++++------- gen/proto/go/accessgraph/v1alpha/azure.pb.go | 2 +- lib/config/configuration.go | 6 + lib/config/fileconf.go | 8 + proto/accessgraph/v1alpha/azure.proto | 2 +- 7 files changed, 794 insertions(+), 465 deletions(-) diff --git a/api/proto/teleport/legacy/types/types.proto b/api/proto/teleport/legacy/types/types.proto index f89b6aca16cf7..41d980906ef9a 100644 --- a/api/proto/teleport/legacy/types/types.proto +++ b/api/proto/teleport/legacy/types/types.proto @@ -8026,12 +8026,14 @@ message OktaOptions { message AccessGraphSync { // AWS is a configuration for AWS Access Graph service poll service. repeated AccessGraphAWSSync AWS = 1 [(gogoproto.jsontag) = "aws,omitempty"]; - // PollInterval is the frequency at which to poll for AWS resources + // PollInterval is the frequency at which to poll for resources google.protobuf.Duration PollInterval = 2 [ (gogoproto.jsontag) = "poll_interval,omitempty", (gogoproto.nullable) = false, (gogoproto.stdduration) = true ]; + // Azure is a configuration for Azure Access Graph service poll service. + repeated AccessGraphAzureSync Azure = 3 [(gogoproto.jsontag) = "azure,omitempty"]; } // AccessGraphAWSSync is a configuration for AWS Access Graph service poll service. @@ -8043,3 +8045,11 @@ message AccessGraphAWSSync { // Integration is the integration name used to generate credentials to interact with AWS APIs. string Integration = 4 [(gogoproto.jsontag) = "integration,omitempty"]; } + +// AccessGraphAzureSync is a configuration for Azure Access Graph service poll service. +message AccessGraphAzureSync { + // SubscriptionID Is the ID of the Azure subscription to sync resources from + string SubscriptionID = 1 [(gogoproto.jsontag) = "subscription_id,omitempty"]; + // Integration is the integration name used to generate credentials to interact with AWS APIs. + string Integration = 2 [(gogoproto.jsontag) = "integration,omitempty"]; +} diff --git a/api/types/discoveryconfig/derived.gen.go b/api/types/discoveryconfig/derived.gen.go index 9053fdd312473..c1f713517c7ca 100644 --- a/api/types/discoveryconfig/derived.gen.go +++ b/api/types/discoveryconfig/derived.gen.go @@ -117,7 +117,8 @@ func deriveTeleportEqual_6(this, that *types.AccessGraphSync) bool { return (this == nil && that == nil) || this != nil && that != nil && deriveTeleportEqual_12(this.AWS, that.AWS) && - this.PollInterval == that.PollInterval + this.PollInterval == that.PollInterval && + deriveTeleportEqual_13(this.Azure, that.Azure) } // deriveTeleportEqual_7 returns whether this and that are equal. @@ -144,12 +145,12 @@ func deriveTeleportEqual_7(this, that map[string]string) bool { func deriveTeleportEqual_8(this, that *types.AWSMatcher) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_13(this.Types, that.Types) && - deriveTeleportEqual_13(this.Regions, that.Regions) && - deriveTeleportEqual_14(this.AssumeRole, that.AssumeRole) && - deriveTeleportEqual_15(this.Tags, that.Tags) && - deriveTeleportEqual_16(this.Params, that.Params) && - deriveTeleportEqual_17(this.SSM, that.SSM) && + deriveTeleportEqual_14(this.Types, that.Types) && + deriveTeleportEqual_14(this.Regions, that.Regions) && + deriveTeleportEqual_15(this.AssumeRole, that.AssumeRole) && + deriveTeleportEqual_16(this.Tags, that.Tags) && + deriveTeleportEqual_17(this.Params, that.Params) && + deriveTeleportEqual_18(this.SSM, that.SSM) && this.Integration == that.Integration && this.KubeAppDiscovery == that.KubeAppDiscovery && this.SetupAccessForARN == that.SetupAccessForARN @@ -159,34 +160,34 @@ func deriveTeleportEqual_8(this, that *types.AWSMatcher) bool { func deriveTeleportEqual_9(this, that *types.AzureMatcher) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_13(this.Subscriptions, that.Subscriptions) && - deriveTeleportEqual_13(this.ResourceGroups, that.ResourceGroups) && - deriveTeleportEqual_13(this.Types, that.Types) && - deriveTeleportEqual_13(this.Regions, that.Regions) && - deriveTeleportEqual_15(this.ResourceTags, that.ResourceTags) && - deriveTeleportEqual_16(this.Params, that.Params) + deriveTeleportEqual_14(this.Subscriptions, that.Subscriptions) && + deriveTeleportEqual_14(this.ResourceGroups, that.ResourceGroups) && + deriveTeleportEqual_14(this.Types, that.Types) && + deriveTeleportEqual_14(this.Regions, that.Regions) && + deriveTeleportEqual_16(this.ResourceTags, that.ResourceTags) && + deriveTeleportEqual_17(this.Params, that.Params) } // deriveTeleportEqual_10 returns whether this and that are equal. func deriveTeleportEqual_10(this, that *types.GCPMatcher) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_13(this.Types, that.Types) && - deriveTeleportEqual_13(this.Locations, that.Locations) && - deriveTeleportEqual_15(this.Tags, that.Tags) && - deriveTeleportEqual_13(this.ProjectIDs, that.ProjectIDs) && - deriveTeleportEqual_13(this.ServiceAccounts, that.ServiceAccounts) && - deriveTeleportEqual_16(this.Params, that.Params) && - deriveTeleportEqual_15(this.Labels, that.Labels) + deriveTeleportEqual_14(this.Types, that.Types) && + deriveTeleportEqual_14(this.Locations, that.Locations) && + deriveTeleportEqual_16(this.Tags, that.Tags) && + deriveTeleportEqual_14(this.ProjectIDs, that.ProjectIDs) && + deriveTeleportEqual_14(this.ServiceAccounts, that.ServiceAccounts) && + deriveTeleportEqual_17(this.Params, that.Params) && + deriveTeleportEqual_16(this.Labels, that.Labels) } // deriveTeleportEqual_11 returns whether this and that are equal. func deriveTeleportEqual_11(this, that *types.KubernetesMatcher) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_13(this.Types, that.Types) && - deriveTeleportEqual_13(this.Namespaces, that.Namespaces) && - deriveTeleportEqual_15(this.Labels, that.Labels) + deriveTeleportEqual_14(this.Types, that.Types) && + deriveTeleportEqual_14(this.Namespaces, that.Namespaces) && + deriveTeleportEqual_16(this.Labels, that.Labels) } // deriveTeleportEqual_12 returns whether this and that are equal. @@ -198,7 +199,7 @@ func deriveTeleportEqual_12(this, that []*types.AccessGraphAWSSync) bool { return false } for i := 0; i < len(this); i++ { - if !(deriveTeleportEqual_18(this[i], that[i])) { + if !(deriveTeleportEqual_19(this[i], that[i])) { return false } } @@ -206,7 +207,7 @@ func deriveTeleportEqual_12(this, that []*types.AccessGraphAWSSync) bool { } // deriveTeleportEqual_13 returns whether this and that are equal. -func deriveTeleportEqual_13(this, that []string) bool { +func deriveTeleportEqual_13(this, that []*types.AccessGraphAzureSync) bool { if this == nil || that == nil { return this == nil && that == nil } @@ -214,7 +215,7 @@ func deriveTeleportEqual_13(this, that []string) bool { return false } for i := 0; i < len(this); i++ { - if !(this[i] == that[i]) { + if !(deriveTeleportEqual_20(this[i], that[i])) { return false } } @@ -222,15 +223,31 @@ func deriveTeleportEqual_13(this, that []string) bool { } // deriveTeleportEqual_14 returns whether this and that are equal. -func deriveTeleportEqual_14(this, that *types.AssumeRole) bool { +func deriveTeleportEqual_14(this, that []string) bool { + if this == nil || that == nil { + return this == nil && that == nil + } + if len(this) != len(that) { + return false + } + for i := 0; i < len(this); i++ { + if !(this[i] == that[i]) { + return false + } + } + return true +} + +// deriveTeleportEqual_15 returns whether this and that are equal. +func deriveTeleportEqual_15(this, that *types.AssumeRole) bool { return (this == nil && that == nil) || this != nil && that != nil && this.RoleARN == that.RoleARN && this.ExternalID == that.ExternalID } -// deriveTeleportEqual_15 returns whether this and that are equal. -func deriveTeleportEqual_15(this, that map[string]utils.Strings) bool { +// deriveTeleportEqual_16 returns whether this and that are equal. +func deriveTeleportEqual_16(this, that map[string]utils.Strings) bool { if this == nil || that == nil { return this == nil && that == nil } @@ -242,15 +259,15 @@ func deriveTeleportEqual_15(this, that map[string]utils.Strings) bool { if !ok { return false } - if !(deriveTeleportEqual_13(v, thatv)) { + if !(deriveTeleportEqual_14(v, thatv)) { return false } } return true } -// deriveTeleportEqual_16 returns whether this and that are equal. -func deriveTeleportEqual_16(this, that *types.InstallerParams) bool { +// deriveTeleportEqual_17 returns whether this and that are equal. +func deriveTeleportEqual_17(this, that *types.InstallerParams) bool { return (this == nil && that == nil) || this != nil && that != nil && this.JoinMethod == that.JoinMethod && @@ -259,28 +276,36 @@ func deriveTeleportEqual_16(this, that *types.InstallerParams) bool { this.InstallTeleport == that.InstallTeleport && this.SSHDConfig == that.SSHDConfig && this.PublicProxyAddr == that.PublicProxyAddr && - deriveTeleportEqual_19(this.Azure, that.Azure) && + deriveTeleportEqual_21(this.Azure, that.Azure) && this.EnrollMode == that.EnrollMode } -// deriveTeleportEqual_17 returns whether this and that are equal. -func deriveTeleportEqual_17(this, that *types.AWSSSM) bool { +// deriveTeleportEqual_18 returns whether this and that are equal. +func deriveTeleportEqual_18(this, that *types.AWSSSM) bool { return (this == nil && that == nil) || this != nil && that != nil && this.DocumentName == that.DocumentName } -// deriveTeleportEqual_18 returns whether this and that are equal. -func deriveTeleportEqual_18(this, that *types.AccessGraphAWSSync) bool { +// deriveTeleportEqual_19 returns whether this and that are equal. +func deriveTeleportEqual_19(this, that *types.AccessGraphAWSSync) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_13(this.Regions, that.Regions) && - deriveTeleportEqual_14(this.AssumeRole, that.AssumeRole) && + deriveTeleportEqual_14(this.Regions, that.Regions) && + deriveTeleportEqual_15(this.AssumeRole, that.AssumeRole) && this.Integration == that.Integration } -// deriveTeleportEqual_19 returns whether this and that are equal. -func deriveTeleportEqual_19(this, that *types.AzureInstallerParams) bool { +// deriveTeleportEqual_20 returns whether this and that are equal. +func deriveTeleportEqual_20(this, that *types.AccessGraphAzureSync) bool { + return (this == nil && that == nil) || + this != nil && that != nil && + this.SubscriptionID == that.SubscriptionID && + this.Integration == that.Integration +} + +// deriveTeleportEqual_21 returns whether this and that are equal. +func deriveTeleportEqual_21(this, that *types.AzureInstallerParams) bool { return (this == nil && that == nil) || this != nil && that != nil && this.ClientID == that.ClientID diff --git a/api/types/types.pb.go b/api/types/types.pb.go index bede358a5c6e9..b8eec25929456 100644 --- a/api/types/types.pb.go +++ b/api/types/types.pb.go @@ -21378,11 +21378,13 @@ var xxx_messageInfo_OktaOptions proto.InternalMessageInfo type AccessGraphSync struct { // AWS is a configuration for AWS Access Graph service poll service. AWS []*AccessGraphAWSSync `protobuf:"bytes,1,rep,name=AWS,proto3" json:"aws,omitempty"` - // PollInterval is the frequency at which to poll for AWS resources - PollInterval time.Duration `protobuf:"bytes,2,opt,name=PollInterval,proto3,stdduration" json:"poll_interval,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + // PollInterval is the frequency at which to poll for resources + PollInterval time.Duration `protobuf:"bytes,2,opt,name=PollInterval,proto3,stdduration" json:"poll_interval,omitempty"` + // Azure is a configuration for Azure Access Graph service poll service. + Azure []*AccessGraphAzureSync `protobuf:"bytes,3,rep,name=Azure,proto3" json:"azure,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *AccessGraphSync) Reset() { *m = AccessGraphSync{} } @@ -21464,6 +21466,50 @@ func (m *AccessGraphAWSSync) XXX_DiscardUnknown() { var xxx_messageInfo_AccessGraphAWSSync proto.InternalMessageInfo +// AccessGraphAzureSync is a configuration for Azure Access Graph service poll service. +type AccessGraphAzureSync struct { + // SubscriptionID Is the ID of the Azure subscription to sync resources from + SubscriptionID string `protobuf:"bytes,1,opt,name=SubscriptionID,proto3" json:"subscription_id,omitempty"` + // Integration is the integration name used to generate credentials to interact with AWS APIs. + Integration string `protobuf:"bytes,2,opt,name=Integration,proto3" json:"integration,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AccessGraphAzureSync) Reset() { *m = AccessGraphAzureSync{} } +func (m *AccessGraphAzureSync) String() string { return proto.CompactTextString(m) } +func (*AccessGraphAzureSync) ProtoMessage() {} +func (*AccessGraphAzureSync) Descriptor() ([]byte, []int) { + return fileDescriptor_9198ee693835762e, []int{372} +} +func (m *AccessGraphAzureSync) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *AccessGraphAzureSync) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_AccessGraphAzureSync.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *AccessGraphAzureSync) XXX_Merge(src proto.Message) { + xxx_messageInfo_AccessGraphAzureSync.Merge(m, src) +} +func (m *AccessGraphAzureSync) XXX_Size() int { + return m.Size() +} +func (m *AccessGraphAzureSync) XXX_DiscardUnknown() { + xxx_messageInfo_AccessGraphAzureSync.DiscardUnknown(m) +} + +var xxx_messageInfo_AccessGraphAzureSync proto.InternalMessageInfo + func init() { proto.RegisterEnum("types.IAMPolicyStatus", IAMPolicyStatus_name, IAMPolicyStatus_value) proto.RegisterEnum("types.DatabaseTLSMode", DatabaseTLSMode_name, DatabaseTLSMode_value) @@ -21901,12 +21947,13 @@ func init() { proto.RegisterType((*OktaOptions)(nil), "types.OktaOptions") proto.RegisterType((*AccessGraphSync)(nil), "types.AccessGraphSync") proto.RegisterType((*AccessGraphAWSSync)(nil), "types.AccessGraphAWSSync") + proto.RegisterType((*AccessGraphAzureSync)(nil), "types.AccessGraphAzureSync") } func init() { proto.RegisterFile("teleport/legacy/types/types.proto", fileDescriptor_9198ee693835762e) } var fileDescriptor_9198ee693835762e = []byte{ - // 30381 bytes of a gzipped FileDescriptorProto + // 30421 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0xbd, 0x6d, 0x70, 0x1c, 0x49, 0x76, 0x20, 0x36, 0xdd, 0x8d, 0x8f, 0xc6, 0xc3, 0x57, 0x23, 0x01, 0x92, 0x20, 0x66, 0x86, 0xcd, 0xa9, 0x99, 0xe1, 0x90, 0xb3, 0x33, 0xe4, 0x12, 0xdc, 0xe1, 0xee, 0xec, 0x7c, 0x6d, 0xa3, 0x1b, @@ -23391,421 +23438,424 @@ var fileDescriptor_9198ee693835762e = []byte{ 0x93, 0xb5, 0x1a, 0xfa, 0x9c, 0xa8, 0x9f, 0x28, 0xd0, 0xc9, 0x25, 0x80, 0xe8, 0x15, 0x9f, 0xbf, 0xb9, 0x98, 0x0a, 0xe4, 0xf3, 0x43, 0xff, 0xe7, 0x2f, 0x16, 0x33, 0x4b, 0x00, 0x79, 0x19, 0x21, 0xc7, 0x58, 0x85, 0x8b, 0x3d, 0xd7, 0x3d, 0xb9, 0x06, 0x85, 0x5d, 0x5b, 0x68, 0xfd, 0xea, 0xfb, - 0x76, 0xbb, 0xfd, 0xff, 0xb3, 0xf7, 0x2d, 0x31, 0x6e, 0x64, 0xd7, 0xa1, 0x2a, 0x92, 0xdd, 0xcd, - 0x3e, 0xec, 0x4f, 0xf5, 0xd5, 0xa7, 0x7b, 0x5a, 0x1a, 0x69, 0x54, 0xa3, 0x91, 0x25, 0x8e, 0x67, - 0x6c, 0x69, 0xde, 0x78, 0x66, 0x6c, 0x8f, 0xc7, 0xd5, 0xec, 0xea, 0x26, 0x25, 0xfe, 0x5c, 0x45, - 0xb6, 0x2c, 0xcb, 0x76, 0xb9, 0x44, 0x56, 0x77, 0x97, 0xcd, 0x66, 0xd1, 0x2c, 0x72, 0x64, 0x19, - 0x0f, 0x78, 0x36, 0x1e, 0x60, 0x03, 0xef, 0x25, 0x71, 0xe2, 0x24, 0xc8, 0x20, 0x1b, 0x2f, 0x62, - 0x04, 0x59, 0x64, 0x1b, 0x24, 0x88, 0xb3, 0xf1, 0xce, 0x80, 0x61, 0xc0, 0x40, 0x76, 0x4e, 0x30, - 0x48, 0x06, 0x48, 0x80, 0x7c, 0x76, 0x41, 0xb2, 0xf0, 0x2a, 0xb8, 0xe7, 0xde, 0x5b, 0x75, 0xeb, - 0x43, 0xaa, 0xe5, 0x19, 0x27, 0x31, 0xe0, 0x55, 0x37, 0xcf, 0x3d, 0xe7, 0xd4, 0xfd, 0xdf, 0x73, - 0xcf, 0x3d, 0x1f, 0x77, 0xc0, 0x77, 0xdc, 0x75, 0x01, 0xaf, 0x30, 0x30, 0xe3, 0xac, 0xbd, 0x05, - 0xe7, 0xb2, 0x06, 0x9c, 0x5c, 0x85, 0x15, 0x39, 0x18, 0x10, 0x67, 0x52, 0x72, 0x46, 0x9e, 0x08, - 0x07, 0xc4, 0x19, 0xfc, 0x40, 0x81, 0x4b, 0xf3, 0xb6, 0x0f, 0xb2, 0x0d, 0xc5, 0xd1, 0xd8, 0xf3, - 0x51, 0x4c, 0xe5, 0xd9, 0x16, 0xc4, 0x6f, 0x4c, 0xa4, 0x80, 0xf2, 0xd4, 0xc4, 0x39, 0xe2, 0x0e, - 0x1e, 0xe6, 0x32, 0x42, 0x3a, 0xce, 0x51, 0x40, 0x5e, 0x84, 0x8d, 0xbe, 0x7b, 0xe8, 0x4c, 0x07, - 0x13, 0x3b, 0xe8, 0x1d, 0xbb, 0x7d, 0x74, 0xc1, 0x42, 0xc3, 0x3d, 0x53, 0xe5, 0x05, 0x96, 0x80, - 0xa7, 0x6a, 0xbc, 0x30, 0xa3, 0xc6, 0x77, 0x0a, 0x45, 0x45, 0xcd, 0x99, 0x68, 0x29, 0xa5, 0x7d, - 0x23, 0x07, 0x5b, 0xb3, 0xd6, 0x0b, 0x79, 0x33, 0xab, 0x0f, 0xd8, 0xc3, 0x85, 0x0c, 0x97, 0x1f, - 0x2e, 0xa4, 0xaf, 0x91, 0xdb, 0x10, 0x3a, 0x50, 0x3d, 0x29, 0x18, 0x82, 0x80, 0x51, 0x9a, 0x91, - 0x13, 0x04, 0x8f, 0xe8, 0x96, 0x90, 0x97, 0x02, 0xea, 0x72, 0x98, 0x4c, 0x23, 0x60, 0xe4, 0x35, - 0x80, 0xde, 0xc0, 0x0f, 0x5c, 0xb4, 0x0f, 0xe0, 0xb2, 0x06, 0x33, 0x0b, 0x0f, 0xa1, 0xf2, 0x83, - 0x30, 0x42, 0x2b, 0x7e, 0xdf, 0xe5, 0x03, 0xe8, 0xc0, 0xe6, 0x8c, 0x0d, 0x92, 0x0e, 0x4f, 0x94, - 0x9d, 0x5e, 0xe4, 0xba, 0x9a, 0x86, 0x39, 0xea, 0x93, 0x3d, 0x9e, 0x9b, 0x35, 0x47, 0x1e, 0x03, - 0x49, 0xef, 0x82, 0x94, 0x3b, 0x37, 0x6e, 0x9e, 0x8e, 0x43, 0xee, 0x0c, 0xd2, 0x1d, 0x0f, 0xc8, - 0x15, 0x28, 0x89, 0x5c, 0x96, 0x54, 0x96, 0x67, 0xcc, 0x81, 0x83, 0xee, 0xba, 0x38, 0x79, 0x30, - 0x62, 0x2a, 0xba, 0xc9, 0x71, 0x29, 0x61, 0x19, 0x21, 0x9d, 0xc7, 0x23, 0xd1, 0xba, 0x4b, 0x62, - 0x7e, 0xc7, 0xcf, 0x26, 0x5e, 0xfa, 0xfb, 0x8a, 0x18, 0xfe, 0xf4, 0xe6, 0xfe, 0xa4, 0xfa, 0x11, - 0x40, 0x2f, 0x25, 0x5e, 0x31, 0xfc, 0x9f, 0x4a, 0x2d, 0x62, 0xd5, 0x71, 0xa9, 0x85, 0xff, 0x24, - 0xd7, 0x61, 0x7d, 0xcc, 0xec, 0x58, 0x27, 0x3e, 0xef, 0x4f, 0x96, 0x37, 0x64, 0x95, 0x81, 0x3b, - 0x3e, 0xf6, 0x29, 0xaf, 0xd7, 0x9d, 0xb0, 0xc3, 0xa4, 0xb3, 0x8e, 0xbc, 0x0c, 0xcb, 0xf4, 0xac, - 0xc3, 0x48, 0x3b, 0x09, 0xf7, 0x08, 0xc4, 0x43, 0xc9, 0xc1, 0x2c, 0x7e, 0x99, 0xff, 0xcf, 0x79, - 0xbd, 0x93, 0x13, 0xcc, 0xe4, 0x93, 0x96, 0x6c, 0xc2, 0x92, 0x3f, 0x3e, 0x92, 0x9a, 0xb6, 0xe8, - 0x8f, 0x8f, 0x68, 0xbb, 0x6e, 0x80, 0xca, 0xbc, 0x75, 0x58, 0xd4, 0x84, 0xe0, 0xf1, 0x90, 0x5d, - 0xc5, 0x8b, 0xe6, 0x1a, 0x83, 0x63, 0xc2, 0xfe, 0xc7, 0xc3, 0x1e, 0xc5, 0x0c, 0x02, 0xdf, 0x96, - 0x03, 0x6c, 0xf1, 0x66, 0xaf, 0x05, 0x81, 0x1f, 0x45, 0xda, 0xea, 0x93, 0x1d, 0x58, 0xa5, 0x7c, - 0xc2, 0x30, 0x5f, 0x5c, 0x10, 0x78, 0x36, 0x2d, 0x08, 0x3c, 0x1e, 0xf6, 0x44, 0x15, 0xcd, 0x95, - 0x40, 0xfa, 0x45, 0xee, 0x82, 0x2a, 0x49, 0x4c, 0xe8, 0xbe, 0x99, 0xb0, 0xa9, 0x8e, 0xd8, 0x48, - 0x92, 0x56, 0x6d, 0x78, 0xe8, 0x9b, 0xeb, 0xbd, 0x38, 0x80, 0x77, 0xcd, 0xf7, 0x14, 0xb1, 0x97, - 0x66, 0x10, 0x11, 0x0d, 0x56, 0x8f, 0x9d, 0xc0, 0x0e, 0x82, 0x13, 0x66, 0x23, 0xc6, 0x03, 0x0b, - 0x97, 0x8e, 0x9d, 0xc0, 0x0a, 0x4e, 0x44, 0xe2, 0x92, 0xf3, 0x14, 0xc7, 0x77, 0xa6, 0x93, 0x63, - 0x5b, 0x96, 0xff, 0x58, 0x8f, 0x9d, 0x3d, 0x76, 0x82, 0x16, 0x2d, 0x93, 0x78, 0x93, 0x6b, 0xb0, - 0x86, 0x7c, 0x7b, 0x9e, 0x60, 0x8c, 0x91, 0x2f, 0xcc, 0x15, 0xca, 0xb8, 0xe7, 0x31, 0xce, 0xbc, - 0x86, 0xff, 0x94, 0x83, 0x0b, 0xd9, 0xbd, 0x83, 0xd3, 0x93, 0xf6, 0x29, 0xfa, 0xe8, 0xf1, 0xba, - 0x2d, 0x53, 0x08, 0x8b, 0x5a, 0x92, 0x35, 0x38, 0xb9, 0xcc, 0xc1, 0x29, 0xc3, 0x06, 0x32, 0xe2, - 0x92, 0xe6, 0xc0, 0x0b, 0x26, 0x3c, 0x18, 0x87, 0xb9, 0x4e, 0x0b, 0xd8, 0x7e, 0x5e, 0xa7, 0x60, - 0xf2, 0x02, 0xac, 0x89, 0x1d, 0xd9, 0x7f, 0x34, 0xa4, 0x1f, 0x66, 0xdb, 0xf1, 0x2a, 0x87, 0xb6, - 0x10, 0x48, 0xce, 0xc3, 0xa2, 0x33, 0x1a, 0xd1, 0x4f, 0xb2, 0x5d, 0x78, 0xc1, 0x19, 0x8d, 0x58, - 0x72, 0x1d, 0xf4, 0x48, 0xb4, 0x0f, 0xd1, 0x4a, 0x88, 0x9b, 0x24, 0x9a, 0x2b, 0x08, 0x64, 0x96, - 0x43, 0x01, 0x5d, 0xf7, 0x94, 0x56, 0xa0, 0x2c, 0x21, 0x0a, 0x38, 0xa3, 0x10, 0xe1, 0x19, 0x28, - 0x8a, 0xf7, 0x6a, 0xe6, 0x58, 0x61, 0x2e, 0x39, 0xfc, 0xad, 0xfa, 0x55, 0xd8, 0xec, 0x7b, 0x01, - 0x4e, 0x5e, 0xd6, 0xa4, 0xd1, 0x88, 0xfb, 0x40, 0xb2, 0x20, 0xbd, 0xe6, 0x39, 0x5e, 0x4c, 0x7b, - 0x52, 0x1f, 0x8d, 0x98, 0x27, 0x24, 0xef, 0xeb, 0xd7, 0x61, 0x9d, 0x4b, 0x5c, 0xfc, 0x88, 0xc4, - 0xba, 0xf0, 0x05, 0x4c, 0xaf, 0x42, 0x3c, 0x9d, 0x11, 0x70, 0x50, 0xad, 0x2f, 0x28, 0xff, 0x56, - 0x81, 0xf3, 0x99, 0x22, 0x1b, 0xf9, 0x12, 0x30, 0x97, 0xaf, 0x89, 0x6f, 0x8f, 0xdd, 0x9e, 0x37, - 0xf2, 0x30, 0x86, 0x06, 0x53, 0x69, 0xde, 0x9e, 0x27, 0xec, 0xa1, 0xfb, 0x58, 0xc7, 0x37, 0x43, - 0x22, 0xa6, 0x6b, 0x51, 0xc7, 0x09, 0xf0, 0xf6, 0x03, 0x38, 0x9f, 0x89, 0x9a, 0xa1, 0x03, 0xf9, - 0x70, 0x3c, 0x99, 0xb4, 0x78, 0xa4, 0x4a, 0x34, 0x5a, 0xd2, 0x8d, 0xf0, 0xe6, 0xfd, 0x30, 0x6c, - 0x5e, 0x42, 0xb8, 0x23, 0x46, 0x72, 0x5d, 0x67, 0xdd, 0x4f, 0x04, 0xd1, 0xec, 0xa5, 0xfd, 0x00, - 0xce, 0xf3, 0xc9, 0x77, 0x34, 0x76, 0x46, 0xc7, 0x11, 0x3b, 0x56, 0xd1, 0x0f, 0x65, 0xb1, 0x63, - 0xb3, 0x72, 0x9f, 0xe2, 0x87, 0x5c, 0xcf, 0x3a, 0x69, 0x20, 0x6f, 0xc3, 0x37, 0x73, 0x62, 0xa9, - 0x67, 0x54, 0x27, 0x63, 0x5a, 0x2b, 0x59, 0xd3, 0xfa, 0xf4, 0x6b, 0xaa, 0x09, 0x44, 0xde, 0xac, - 0x98, 0xd6, 0x93, 0x1b, 0x54, 0x09, 0x39, 0x9d, 0x57, 0x44, 0xda, 0x1a, 0x2c, 0x96, 0xcc, 0x73, - 0xa3, 0x97, 0x04, 0x91, 0x8b, 0xb0, 0x1c, 0xe6, 0xcb, 0xe6, 0x07, 0x47, 0x91, 0x01, 0x6a, 0x7d, - 0xf2, 0x1c, 0xac, 0x30, 0x91, 0x3c, 0xb6, 0xe6, 0x00, 0x61, 0x3a, 0x5d, 0x78, 0xa2, 0x0f, 0x14, - 0x78, 0xee, 0x49, 0x7d, 0x48, 0xee, 0xc1, 0x05, 0x34, 0xeb, 0x08, 0xfc, 0x70, 0x18, 0xec, 0x9e, - 0xd3, 0x3b, 0x76, 0xf9, 0xac, 0xd5, 0x32, 0x07, 0x63, 0x34, 0xb2, 0xac, 0x96, 0x34, 0x0e, 0xa3, - 0x91, 0x15, 0xf8, 0xe2, 0x77, 0x85, 0x92, 0xf3, 0x3a, 0xf4, 0xe1, 0xe2, 0x1c, 0x4a, 0x69, 0xe3, - 0x50, 0xe4, 0x8d, 0xe3, 0x06, 0xa8, 0x87, 0x6e, 0x9f, 0xca, 0xc4, 0x6e, 0x1f, 0xab, 0xf6, 0xf6, - 0x6d, 0x96, 0x21, 0xde, 0x5c, 0x0b, 0xe1, 0x56, 0xe0, 0x1f, 0xdc, 0xe6, 0x5f, 0x39, 0x11, 0x47, - 0x9e, 0x7c, 0xad, 0x20, 0x2f, 0xc3, 0xd9, 0x44, 0x7c, 0x92, 0xc8, 0xe1, 0xdd, 0xdc, 0xa0, 0x45, - 0xf1, 0x68, 0x56, 0x57, 0x61, 0x45, 0xcc, 0x8a, 0x71, 0xe8, 0x07, 0x67, 0x96, 0x38, 0x8c, 0xae, - 0x3a, 0xfe, 0xb9, 0xa9, 0x68, 0x54, 0xe6, 0x8d, 0xe4, 0x14, 0xb2, 0x34, 0x79, 0x09, 0x48, 0x28, - 0xb7, 0x87, 0x1b, 0x05, 0xff, 0xe0, 0x86, 0x28, 0x09, 0x57, 0x38, 0xff, 0xec, 0x5f, 0xe5, 0xe0, - 0x6c, 0xc6, 0x55, 0x86, 0x5e, 0x02, 0xbc, 0xe1, 0xc4, 0x3d, 0x62, 0x57, 0x08, 0xb9, 0x91, 0xeb, - 0x12, 0x9c, 0xeb, 0xa7, 0x16, 0x59, 0x06, 0x74, 0xfe, 0x2d, 0xfe, 0x8b, 0x6e, 0x1e, 0xce, 0x58, - 0xa8, 0x5e, 0xe8, 0xbf, 0xa4, 0x06, 0x1b, 0x98, 0xd6, 0x21, 0xf0, 0x7c, 0xcc, 0x0e, 0x81, 0x42, - 0x48, 0x21, 0x76, 0xd9, 0xc1, 0x5a, 0xb4, 0x25, 0x24, 0x2a, 0x85, 0x98, 0xea, 0x28, 0x01, 0x21, - 0x9f, 0x80, 0x6d, 0xe9, 0xac, 0xb1, 0x13, 0x2b, 0x0f, 0x2d, 0xdd, 0xcd, 0x4d, 0x27, 0x3c, 0x75, - 0x76, 0x63, 0x6b, 0x70, 0x07, 0x2e, 0xe3, 0x20, 0x7a, 0xfd, 0x91, 0x9d, 0xca, 0x03, 0x82, 0x4d, - 0x65, 0x81, 0xf3, 0xb7, 0x29, 0x56, 0xad, 0x3f, 0x4a, 0xa4, 0x04, 0xa1, 0xad, 0xe6, 0xdd, 0xf7, - 0x00, 0xce, 0x67, 0xd6, 0x98, 0x1e, 0x30, 0x68, 0x48, 0x15, 0xc9, 0x46, 0x4b, 0xf4, 0x37, 0x15, - 0x8e, 0xae, 0xc2, 0xca, 0x43, 0xd7, 0x19, 0xbb, 0x63, 0x7e, 0x72, 0xf3, 0x29, 0xc1, 0x60, 0xf2, - 0xc1, 0xdd, 0x8f, 0x0f, 0x0d, 0xd7, 0x19, 0x91, 0x06, 0x9c, 0x65, 0x27, 0xa0, 0x77, 0x82, 0xc2, - 0x20, 0xd7, 0x33, 0x29, 0x31, 0x71, 0x08, 0x49, 0xf0, 0x68, 0xaa, 0x21, 0x16, 0xa3, 0x36, 0x37, - 0x8e, 0x92, 0x20, 0xba, 0xa2, 0x2f, 0x64, 0x63, 0x93, 0x1d, 0x28, 0x31, 0xe6, 0xec, 0x5a, 0xc0, - 0x1e, 0x08, 0xae, 0xce, 0xfd, 0x42, 0x05, 0xed, 0x8b, 0x83, 0xf0, 0x7f, 0x7a, 0x5e, 0xe3, 0x5b, - 0xac, 0x7d, 0x22, 0xbf, 0x7f, 0x98, 0x2b, 0x08, 0xe4, 0xef, 0x1e, 0xda, 0x5f, 0x2b, 0xa2, 0xa9, - 0xb1, 0xcb, 0x31, 0x9d, 0x5a, 0x81, 0x3b, 0x14, 0x6f, 0x40, 0xcb, 0x26, 0xff, 0xf5, 0x94, 0x53, - 0x9d, 0xbc, 0x06, 0x2b, 0x94, 0xed, 0xd1, 0x74, 0xc8, 0xa6, 0x5c, 0x3e, 0x16, 0x97, 0xa7, 0xc1, - 0x8a, 0xe8, 0xb0, 0x55, 0xcf, 0x98, 0xa5, 0x93, 0xe8, 0x27, 0x95, 0x96, 0x83, 0x93, 0xc9, 0x48, - 0x9e, 0xa8, 0x42, 0x51, 0x68, 0x35, 0x3a, 0x6d, 0x4e, 0x52, 0xa4, 0x38, 0x91, 0xb4, 0xbc, 0xb3, - 0xc8, 0x54, 0x85, 0xda, 0x8b, 0x50, 0x92, 0x78, 0xd3, 0xc6, 0x30, 0xcf, 0x19, 0xd1, 0x18, 0xf6, - 0x8b, 0x0f, 0xf6, 0x43, 0x28, 0x0a, 0x96, 0xf4, 0x5a, 0x70, 0xec, 0x07, 0x62, 0x91, 0xe3, 0xff, - 0x14, 0x46, 0x7b, 0x19, 0x1b, 0xb9, 0x60, 0xe2, 0xff, 0x78, 0x96, 0x4c, 0x1c, 0x7a, 0x1f, 0x18, - 0x04, 0xf6, 0x08, 0x2d, 0xb0, 0x42, 0xe1, 0x99, 0xc2, 0x3b, 0x83, 0x80, 0xd9, 0x65, 0xf1, 0x6f, - 0xfc, 0x45, 0x78, 0x08, 0x27, 0xb4, 0x09, 0xb3, 0xf6, 0xcc, 0xd8, 0x91, 0x91, 0x4b, 0x1f, 0x19, - 0x2c, 0xde, 0x0a, 0xa7, 0x64, 0x5f, 0x06, 0x84, 0xe1, 0x91, 0x21, 0xed, 0x0c, 0x85, 0xd8, 0xce, - 0x20, 0xdd, 0xc9, 0xa3, 0xd1, 0x63, 0x27, 0x8e, 0xb8, 0x93, 0x27, 0xf7, 0xa9, 0x3f, 0xce, 0x09, - 0x15, 0xc1, 0x8e, 0xef, 0x4f, 0x82, 0xc9, 0xd8, 0x19, 0xc5, 0x54, 0xa1, 0xe4, 0x04, 0x9e, 0x41, - 0x09, 0xfa, 0x36, 0xa6, 0xd0, 0xf0, 0xc7, 0x22, 0xc4, 0x47, 0x38, 0x73, 0x4b, 0xb7, 0x3f, 0x12, - 0x97, 0xf1, 0x75, 0x8a, 0xad, 0xcb, 0xc8, 0x74, 0xc2, 0x4a, 0x5c, 0xab, 0x67, 0xcc, 0x4d, 0xc6, - 0x33, 0x85, 0x45, 0xaa, 0x19, 0x8b, 0x38, 0xa9, 0x0b, 0xdd, 0x89, 0x56, 0x74, 0x9c, 0xab, 0xbc, - 0xd6, 0xc9, 0xa7, 0x60, 0xd9, 0xeb, 0xcb, 0x99, 0x22, 0x93, 0x5a, 0xb8, 0x5a, 0x9f, 0x45, 0xab, - 0x8e, 0x78, 0xd0, 0x39, 0xe7, 0x71, 0xe8, 0xce, 0x6a, 0x4c, 0x69, 0xac, 0xed, 0x88, 0xdb, 0x68, - 0x9a, 0x8c, 0xac, 0x41, 0x2e, 0x1c, 0xe1, 0x9c, 0xd7, 0x67, 0xcb, 0x2b, 0x8a, 0x97, 0x6d, 0xf2, - 0x5f, 0xda, 0xff, 0x86, 0x1b, 0xa7, 0xed, 0x23, 0xba, 0x14, 0x67, 0x74, 0xf8, 0x32, 0x0b, 0x55, - 0x19, 0xef, 0xb7, 0xab, 0x20, 0x87, 0xfb, 0xf5, 0xc4, 0xe6, 0x27, 0x60, 0xdd, 0xb1, 0xa7, 0xfd, - 0x79, 0x1e, 0xd6, 0xe2, 0x6a, 0x72, 0xf2, 0x22, 0x14, 0xa4, 0x1d, 0x68, 0x33, 0x43, 0x97, 0x8e, - 0xfb, 0x0e, 0x22, 0x9d, 0x6a, 0xc7, 0x21, 0x77, 0x60, 0x0d, 0x0d, 0xf7, 0x50, 0xf4, 0x9c, 0x78, - 0xfc, 0xf1, 0x65, 0xfe, 0xfb, 0x59, 0xf1, 0x47, 0xef, 0x5e, 0x39, 0x83, 0x4f, 0x65, 0x2b, 0x94, - 0x96, 0x4a, 0x7f, 0xb4, 0x50, 0xd2, 0x82, 0x16, 0x66, 0x6b, 0x41, 0x79, 0x53, 0x66, 0x68, 0x41, - 0x17, 0xe6, 0x68, 0x41, 0x23, 0x4a, 0x59, 0x0b, 0x8a, 0xba, 0xf0, 0xa5, 0x59, 0xba, 0xf0, 0x88, - 0x86, 0xe9, 0xc2, 0x23, 0x2d, 0x66, 0x71, 0xa6, 0x16, 0x33, 0xa2, 0xe1, 0x5a, 0xcc, 0x6b, 0xbc, - 0x8f, 0xc6, 0xce, 0x23, 0x1b, 0x3b, 0x8f, 0x1f, 0x8b, 0xd8, 0x7a, 0xd3, 0x79, 0x84, 0xc6, 0x35, - 0x3b, 0xcb, 0x20, 0x2c, 0x72, 0xb4, 0xdf, 0x55, 0x12, 0x9a, 0x40, 0x31, 0x7e, 0x2f, 0xc0, 0x1a, - 0x3b, 0xac, 0x78, 0x38, 0x53, 0x76, 0x5a, 0xad, 0x9a, 0xab, 0x02, 0xca, 0xee, 0x9b, 0x1f, 0x82, - 0xf5, 0x10, 0x8d, 0x5f, 0xb9, 0xd0, 0x53, 0xcf, 0x0c, 0xa9, 0x79, 0xd8, 0x99, 0x17, 0x61, 0x23, - 0x44, 0xe4, 0xda, 0x1c, 0x76, 0xdd, 0x5c, 0x35, 0x55, 0x51, 0xd0, 0xe6, 0x70, 0xed, 0x28, 0x79, - 0xf3, 0xf8, 0x25, 0xd5, 0x4a, 0xfb, 0x61, 0x3e, 0xa6, 0x25, 0x11, 0x9f, 0xa1, 0xa7, 0x68, 0xe0, - 0xdb, 0xbc, 0x93, 0xf8, 0x5e, 0x74, 0x75, 0xc6, 0x98, 0x71, 0x9b, 0x26, 0xcb, 0x6a, 0x99, 0x10, - 0x04, 0xbe, 0x30, 0x71, 0xb2, 0x99, 0x44, 0xcd, 0xce, 0x7d, 0x9c, 0xb3, 0x82, 0x1d, 0xdb, 0x78, - 0xca, 0xf3, 0xd9, 0x89, 0x6b, 0x2a, 0x9d, 0xb2, 0x28, 0x59, 0x87, 0xbf, 0xc4, 0x07, 0xba, 0x80, - 0x4a, 0xc5, 0x20, 0xce, 0x3c, 0x9f, 0x71, 0x77, 0x4a, 0x31, 0xc7, 0x5e, 0x42, 0xce, 0xea, 0x54, - 0xfc, 0x2b, 0xd8, 0x1a, 0xb0, 0x82, 0x3a, 0x0a, 0xc1, 0xb0, 0x90, 0xa1, 0x82, 0x4f, 0x37, 0xbe, - 0x52, 0x6b, 0x98, 0x25, 0x4a, 0x27, 0xd8, 0x1c, 0xc3, 0x33, 0xb2, 0x66, 0x21, 0x5e, 0xc9, 0x05, - 0x11, 0xc5, 0x77, 0x6e, 0x0f, 0x44, 0x0a, 0x08, 0xac, 0xea, 0x05, 0x27, 0x0e, 0xe0, 0x68, 0xda, - 0x31, 0x6c, 0xcf, 0x1e, 0x92, 0x39, 0x19, 0xa2, 0xa2, 0x03, 0x34, 0x27, 0x1f, 0xa0, 0xb2, 0x9e, - 0x21, 0x1f, 0xd3, 0x33, 0x68, 0x7f, 0x94, 0x87, 0xe7, 0x4f, 0x31, 0x5c, 0x73, 0xbe, 0xf9, 0xe9, - 0xb8, 0x78, 0x96, 0x8b, 0xdd, 0x0c, 0x29, 0x53, 0xbe, 0x41, 0xd2, 0x5b, 0x6a, 0xb6, 0x70, 0xf6, - 0x25, 0x58, 0x67, 0xbb, 0x20, 0x33, 0x4b, 0x3c, 0x9c, 0x0e, 0x4e, 0xb1, 0x0d, 0x5e, 0x14, 0x3e, - 0x54, 0x09, 0x52, 0xdc, 0x19, 0x71, 0xc7, 0xb0, 0x42, 0x18, 0xe9, 0x40, 0x09, 0xd1, 0x0e, 0x1d, - 0x6f, 0x70, 0x2a, 0x67, 0x1e, 0xe1, 0xa1, 0x25, 0x93, 0x31, 0x6b, 0x6a, 0x0a, 0xd8, 0xc3, 0xdf, - 0xe4, 0x3a, 0xac, 0x0f, 0xa7, 0x27, 0x54, 0xf0, 0x60, 0x73, 0x81, 0x5b, 0x7f, 0x2c, 0x98, 0xab, - 0xc3, 0xe9, 0x89, 0x3e, 0x1a, 0xe1, 0x90, 0xa2, 0x99, 0xc8, 0x06, 0xc5, 0x63, 0xab, 0x56, 0x60, - 0x2e, 0x22, 0x26, 0x65, 0xc0, 0xd6, 0x2d, 0xc7, 0x3d, 0x07, 0xcc, 0x68, 0x90, 0x67, 0xc8, 0x62, - 0x3f, 0xb4, 0xff, 0xc8, 0x89, 0xfb, 0xee, 0xec, 0x79, 0xff, 0xeb, 0x21, 0xca, 0x18, 0xa2, 0x1b, - 0xa0, 0xd2, 0xae, 0x8f, 0x36, 0x95, 0x70, 0x8c, 0xd6, 0x86, 0xd3, 0x93, 0xb0, 0xef, 0xe4, 0x8e, - 0x5f, 0x94, 0x3b, 0xfe, 0x35, 0x71, 0x1f, 0xce, 0xdc, 0x1e, 0x66, 0x77, 0xb9, 0xf6, 0xaf, 0x79, - 0xb8, 0x7e, 0xba, 0x4d, 0xe0, 0xd7, 0xe3, 0x96, 0x31, 0x6e, 0x09, 0xd5, 0xe9, 0x42, 0x4a, 0x75, - 0x9a, 0xb1, 0xf6, 0x16, 0xb3, 0xd6, 0x5e, 0x4a, 0x51, 0xbb, 0x94, 0xa1, 0xa8, 0xcd, 0x5c, 0xa0, - 0xc5, 0x27, 0x2c, 0xd0, 0x65, 0x79, 0x9e, 0xfc, 0x63, 0xa8, 0xc0, 0x88, 0xdf, 0x07, 0x1e, 0xc0, - 0x59, 0x71, 0x1f, 0x60, 0x27, 0x47, 0xa4, 0x7f, 0x2f, 0xdd, 0xbe, 0x99, 0x75, 0x13, 0x40, 0xb4, - 0x0c, 0x69, 0x7d, 0x83, 0xdf, 0x01, 0xa2, 0xf2, 0xff, 0x39, 0xd2, 0x3f, 0xb9, 0x0f, 0x17, 0x30, - 0xbe, 0x7c, 0x4f, 0x7e, 0x39, 0xb0, 0xc7, 0xee, 0x21, 0x9f, 0x0f, 0x57, 0x53, 0xb2, 0xb2, 0xd7, - 0x93, 0xaa, 0x63, 0xba, 0x87, 0xd5, 0x33, 0xe6, 0xb9, 0x20, 0x03, 0x9e, 0xbc, 0x58, 0xfc, 0xa9, - 0x02, 0xda, 0x93, 0xfb, 0x0b, 0x15, 0x55, 0xc9, 0x0e, 0x5f, 0x36, 0x4b, 0x8e, 0xd4, 0x7b, 0xcf, - 0xc3, 0xea, 0xd8, 0x3d, 0x1c, 0xbb, 0xc1, 0x71, 0x4c, 0x03, 0xb2, 0xc2, 0x81, 0xa2, 0x63, 0x44, - 0x50, 0xca, 0xa7, 0x92, 0xcc, 0x05, 0x91, 0xb6, 0x17, 0xde, 0x17, 0x33, 0xc7, 0x81, 0xce, 0x26, - 0xb9, 0x82, 0xec, 0xc7, 0x9d, 0x42, 0x31, 0xa7, 0xe6, 0x4d, 0x1e, 0x3a, 0xf3, 0xd0, 0x1b, 0xb8, - 0xda, 0x5f, 0x2a, 0x42, 0x22, 0xc8, 0xea, 0x3c, 0xf2, 0x40, 0x32, 0xe6, 0xcd, 0xa7, 0xc4, 0x90, - 0x2c, 0x12, 0xd9, 0xee, 0x91, 0x87, 0x67, 0x44, 0x40, 0x2c, 0x3c, 0x23, 0x42, 0xde, 0x87, 0x45, - 0x22, 0xbf, 0x35, 0xbf, 0x21, 0x2c, 0x82, 0xe8, 0x9e, 0x77, 0x70, 0x8b, 0xdc, 0x84, 0x25, 0x66, - 0x04, 0x24, 0xaa, 0xbb, 0x1e, 0xab, 0xee, 0xc1, 0x2d, 0x53, 0x94, 0x6b, 0xef, 0x84, 0xef, 0x5a, - 0xa9, 0x46, 0x1c, 0xdc, 0x22, 0xaf, 0x9d, 0xce, 0x38, 0xb7, 0x28, 0x8c, 0x73, 0x43, 0xc3, 0xdc, - 0xd7, 0x63, 0x86, 0xb9, 0xd7, 0xe6, 0xf7, 0x16, 0x7f, 0x8d, 0x64, 0xe1, 0x08, 0xa3, 0x30, 0x55, - 0x3f, 0xcb, 0xc1, 0xb3, 0x73, 0x29, 0xc8, 0x25, 0x28, 0xea, 0xed, 0x5a, 0x27, 0x1a, 0x5f, 0xba, - 0x66, 0x04, 0x84, 0xec, 0xc3, 0xf2, 0x8e, 0x13, 0x78, 0x3d, 0x3a, 0x8d, 0x33, 0x9f, 0x07, 0x52, - 0x6c, 0x43, 0xf4, 0xea, 0x19, 0x33, 0xa2, 0x25, 0x36, 0x6c, 0xe0, 0x5a, 0x88, 0xa5, 0x9e, 0xca, - 0x67, 0xe8, 0x1a, 0x52, 0x0c, 0x53, 0x64, 0x74, 0x9f, 0x49, 0x01, 0xc9, 0x43, 0x20, 0x96, 0x55, - 0xad, 0xb8, 0xe3, 0x09, 0xbf, 0x83, 0x4f, 0xbc, 0xd0, 0xd2, 0xf3, 0xa3, 0x4f, 0xe8, 0xbb, 0x14, - 0x5d, 0xf5, 0x8c, 0x99, 0xc1, 0x2d, 0xb9, 0xcc, 0xdf, 0x16, 0xf2, 0xce, 0xec, 0x4e, 0x78, 0x8a, - 0x50, 0xaf, 0x37, 0xa0, 0xd8, 0x16, 0xb6, 0x08, 0x92, 0xc5, 0xbc, 0xb0, 0x3b, 0x30, 0xc3, 0x52, - 0xed, 0x37, 0x14, 0xa1, 0x74, 0x78, 0x72, 0x67, 0x49, 0x99, 0xc1, 0xfa, 0xf3, 0x33, 0x83, 0xf5, - 0x7f, 0xc1, 0xcc, 0x60, 0x9a, 0x07, 0x37, 0x4f, 0xdd, 0xb1, 0xe4, 0x93, 0xa0, 0x62, 0x12, 0x25, - 0x47, 0x1a, 0x24, 0xb6, 0xbe, 0x36, 0xc2, 0xd8, 0xdf, 0x55, 0x9e, 0xa9, 0xce, 0x5c, 0xef, 0xc5, - 0xa9, 0xb5, 0x3f, 0xe1, 0x31, 0xdf, 0x6b, 0xfd, 0x76, 0x42, 0xd1, 0xfc, 0x7e, 0x9d, 0x2c, 0x8c, - 0xd8, 0x62, 0x7b, 0x5e, 0x4a, 0x62, 0x99, 0xfe, 0xd6, 0x6c, 0x5f, 0x0b, 0x69, 0xe5, 0xfd, 0x41, - 0x1e, 0x2e, 0xcd, 0x23, 0xcf, 0x4c, 0x93, 0xad, 0x3c, 0x5d, 0x9a, 0xec, 0x9b, 0x50, 0x64, 0xb0, - 0xd0, 0x83, 0x00, 0xc7, 0x96, 0x93, 0xd2, 0xb1, 0x15, 0xc5, 0xe4, 0x79, 0x58, 0xd4, 0x2b, 0x56, - 0x94, 0xb9, 0x0d, 0x4d, 0x7d, 0x9d, 0x5e, 0x80, 0x46, 0xa4, 0xbc, 0x88, 0x7c, 0x31, 0x9d, 0xac, - 0x90, 0xa7, 0x6c, 0xbb, 0x28, 0x75, 0x48, 0x2a, 0x1d, 0x03, 0xd6, 0x37, 0x4a, 0x1f, 0xc0, 0x23, - 0x72, 0x9b, 0xe9, 0xc4, 0x87, 0x1a, 0x2c, 0xb6, 0xc7, 0x6e, 0xe0, 0x4e, 0x64, 0x33, 0xdc, 0x11, - 0x42, 0x4c, 0x5e, 0xc2, 0x8d, 0x64, 0x9d, 0xc7, 0x2c, 0x26, 0xc2, 0xa2, 0x1c, 0xa7, 0x06, 0xad, - 0x6a, 0x29, 0xd8, 0x94, 0x50, 0x28, 0x41, 0xdd, 0x99, 0x0e, 0x7b, 0xc7, 0x5d, 0xb3, 0xce, 0x25, - 0x27, 0x46, 0x30, 0x40, 0x28, 0x6d, 0x60, 0x60, 0x4a, 0x28, 0xda, 0xb7, 0x15, 0x38, 0x97, 0xd5, - 0x0e, 0x72, 0x09, 0x0a, 0xc3, 0xcc, 0xbc, 0x8c, 0x43, 0xe6, 0xca, 0x5d, 0xa2, 0x7f, 0xed, 0x43, - 0x7f, 0x7c, 0xe2, 0x4c, 0x64, 0x63, 0x65, 0x09, 0x6c, 0x02, 0xfd, 0xb1, 0x87, 0xff, 0x93, 0x2b, - 0xe2, 0xc8, 0xc9, 0xa7, 0x32, 0x39, 0xe2, 0x1f, 0x4d, 0x07, 0xa8, 0xf5, 0xdb, 0xad, 0x11, 0x4b, - 0x07, 0xf0, 0x0a, 0x14, 0x68, 0xb5, 0x12, 0xb3, 0x97, 0xce, 0x1f, 0xbd, 0x51, 0xe7, 0x48, 0xac, - 0x56, 0x81, 0x73, 0x32, 0x30, 0x11, 0x59, 0xbb, 0x07, 0x6b, 0x71, 0x0c, 0x62, 0xc4, 0x23, 0xc2, - 0x96, 0x6e, 0xab, 0x9c, 0xd3, 0x8e, 0xef, 0x33, 0x87, 0x99, 0x9d, 0x67, 0x7e, 0xf6, 0xee, 0x15, - 0xa0, 0x3f, 0x19, 0x4d, 0x56, 0xc4, 0x58, 0xed, 0x3b, 0x39, 0x38, 0x17, 0xf9, 0xe8, 0x8b, 0x35, - 0xf4, 0x2b, 0xeb, 0x30, 0xaa, 0xc7, 0x1c, 0x1a, 0x85, 0xdc, 0x98, 0x6e, 0xe0, 0x1c, 0x3f, 0xaa, - 0x7d, 0xd8, 0x9a, 0x85, 0x4f, 0x5e, 0x84, 0x65, 0x0c, 0xeb, 0x34, 0x72, 0x7a, 0xae, 0xbc, 0xcd, - 0x0e, 0x05, 0xd0, 0x8c, 0xca, 0xb5, 0x9f, 0x28, 0xb0, 0xcd, 0xdd, 0x3c, 0x1a, 0x8e, 0x37, 0xc4, - 0x57, 0x82, 0x9e, 0xfb, 0xc1, 0x38, 0x3c, 0xef, 0xc7, 0xf6, 0xb1, 0x17, 0xe2, 0xde, 0x3c, 0xa9, - 0xaf, 0xcd, 0x6e, 0x2d, 0xb9, 0x89, 0xa1, 0xca, 0xf8, 0x2b, 0x7a, 0x81, 0x05, 0x98, 0x18, 0x52, - 0x80, 0x1c, 0x60, 0x02, 0x31, 0xb4, 0xff, 0x03, 0x97, 0xe7, 0x7f, 0x80, 0x7c, 0x01, 0x56, 0x31, - 0xf7, 0x56, 0x77, 0x74, 0x34, 0x76, 0xfa, 0xae, 0xd0, 0xec, 0x09, 0x6d, 0xac, 0x5c, 0xc6, 0x22, - 0xaf, 0xf1, 0x80, 0x07, 0x47, 0x98, 0xd5, 0x8b, 0x13, 0xc5, 0x7c, 0xa9, 0x64, 0x6e, 0xda, 0x37, - 0x14, 0x20, 0x69, 0x1e, 0xe4, 0x63, 0xb0, 0xd2, 0xed, 0x54, 0xac, 0x89, 0x33, 0x9e, 0x54, 0xfd, - 0xe9, 0x98, 0x87, 0x3d, 0x63, 0xfe, 0xef, 0x93, 0x9e, 0xcd, 0xde, 0x83, 0x8e, 0xfd, 0xe9, 0xd8, - 0x8c, 0xe1, 0x61, 0x8e, 0x27, 0xd7, 0xfd, 0x4a, 0xdf, 0x79, 0x1c, 0xcf, 0xf1, 0xc4, 0x61, 0xb1, - 0x1c, 0x4f, 0x1c, 0xa6, 0x7d, 0x5f, 0x81, 0x8b, 0xc2, 0x38, 0xb2, 0x9f, 0x51, 0x97, 0x0a, 0x46, - 0x79, 0x19, 0x8b, 0x38, 0xbb, 0xf3, 0x24, 0xf4, 0x0d, 0x11, 0x08, 0x09, 0x2b, 0x88, 0xa2, 0x3a, - 0xa3, 0x25, 0x9f, 0x86, 0x82, 0x35, 0xf1, 0x47, 0xa7, 0x88, 0x84, 0xa4, 0x86, 0x23, 0x3a, 0xf1, - 0x47, 0xc8, 0x02, 0x29, 0x35, 0x17, 0xce, 0xc9, 0x95, 0x13, 0x35, 0x26, 0x0d, 0x58, 0xe2, 0x21, - 0xef, 0x12, 0x76, 0x07, 0x73, 0xda, 0xb4, 0xb3, 0x2e, 0xc2, 0x2d, 0xf1, 0x38, 0xaf, 0xa6, 0xe0, - 0xa1, 0xfd, 0x96, 0x02, 0x25, 0x2a, 0xd8, 0xe0, 0xa5, 0xf4, 0xfd, 0x4e, 0xe9, 0xb8, 0x1c, 0x2c, - 0xcc, 0x68, 0x42, 0xf6, 0xa7, 0x3a, 0x8d, 0x5f, 0x85, 0xf5, 0x04, 0x01, 0xd1, 0x30, 0xd0, 0xc6, - 0xc0, 0xeb, 0x39, 0x2c, 0x65, 0x0c, 0x33, 0x41, 0x89, 0xc1, 0xb4, 0xff, 0xa7, 0xc0, 0xb9, 0xd6, - 0x57, 0x26, 0x0e, 0x7b, 0xb6, 0x35, 0xa7, 0x03, 0xb1, 0xde, 0xa9, 0xb0, 0x26, 0xac, 0x6c, 0x59, - 0x10, 0x00, 0x26, 0xac, 0x71, 0x98, 0x19, 0x96, 0x92, 0x2a, 0x14, 0xf9, 0xf9, 0x12, 0xf0, 0xf0, - 0xac, 0x97, 0x25, 0xdd, 0x48, 0xc4, 0x98, 0x23, 0xd1, 0x96, 0xe0, 0x16, 0xc6, 0x69, 0xcc, 0x90, - 0x5a, 0xfb, 0x37, 0x05, 0x36, 0x67, 0xd0, 0x90, 0x37, 0x61, 0x01, 0x1d, 0x14, 0xf9, 0xe8, 0x5d, - 0x9a, 0xf1, 0x89, 0x49, 0xef, 0xf8, 0xe0, 0x16, 0x3b, 0x88, 0x4e, 0xe8, 0x0f, 0x93, 0x51, 0x91, - 0x07, 0xb0, 0xac, 0xf7, 0xfb, 0xfc, 0x76, 0x96, 0x8b, 0xdd, 0xce, 0x66, 0x7c, 0xf1, 0xe5, 0x10, - 0x9f, 0xdd, 0xce, 0x98, 0xab, 0x4c, 0xbf, 0x6f, 0x73, 0xe7, 0xcb, 0x88, 0xdf, 0xf6, 0x27, 0x61, - 0x2d, 0x8e, 0xfc, 0x54, 0xfe, 0x62, 0xef, 0x28, 0xa0, 0xc6, 0xeb, 0xf0, 0xcb, 0x09, 0x14, 0x95, - 0x35, 0xcc, 0x4f, 0x98, 0x54, 0xbf, 0x93, 0x83, 0xf3, 0x99, 0x3d, 0x4c, 0x5e, 0x82, 0x45, 0x7d, - 0x34, 0xaa, 0xed, 0xf2, 0x59, 0xc5, 0x25, 0x24, 0x54, 0x7a, 0xc7, 0x2e, 0xaf, 0x0c, 0x89, 0xbc, - 0x02, 0x45, 0x66, 0x1d, 0xb0, 0x2b, 0x36, 0x1c, 0x8c, 0x7c, 0xc3, 0x4d, 0x17, 0xe2, 0x81, 0x52, - 0x05, 0x22, 0xd9, 0x83, 0x35, 0x1e, 0x33, 0xc6, 0x74, 0x8f, 0xdc, 0xaf, 0x85, 0x11, 0xfb, 0x31, - 0xa9, 0x80, 0xd0, 0xa4, 0xdb, 0x63, 0x56, 0x26, 0x47, 0x4d, 0x89, 0x53, 0x91, 0x3a, 0xa8, 0xc8, - 0x53, 0xe6, 0xc4, 0xa2, 0xb5, 0x62, 0x14, 0x1f, 0x56, 0x89, 0x19, 0xbc, 0x52, 0x94, 0xe1, 0x70, - 0xe9, 0x41, 0xe0, 0x1d, 0x0d, 0x4f, 0xdc, 0xe1, 0xe4, 0x97, 0x37, 0x5c, 0xd1, 0x37, 0x4e, 0x35, - 0x5c, 0xbf, 0x57, 0x60, 0x8b, 0x39, 0x49, 0x46, 0x25, 0x1a, 0x29, 0x40, 0x37, 0x4a, 0x34, 0xf4, - 0x7e, 0xc6, 0xa3, 0xa2, 0xec, 0xc2, 0x12, 0x8b, 0x56, 0x23, 0x56, 0xc6, 0xb3, 0x99, 0x55, 0x60, - 0x38, 0x07, 0xb7, 0x98, 0xf8, 0xc2, 0x3c, 0x25, 0x03, 0x53, 0x90, 0x92, 0x03, 0x28, 0x55, 0x06, - 0xae, 0x33, 0x9c, 0x8e, 0x3a, 0xa7, 0x7b, 0x41, 0xdd, 0xe2, 0x6d, 0x59, 0xe9, 0x31, 0x32, 0x7c, - 0x79, 0xc5, 0x9d, 0x5c, 0x66, 0x44, 0x3a, 0xa1, 0xf3, 0x54, 0x01, 0x15, 0xaf, 0x1f, 0x9d, 0xd3, - 0x3f, 0x49, 0x20, 0xd2, 0xc5, 0x3d, 0x03, 0xb9, 0x77, 0x95, 0x0d, 0x6b, 0x75, 0x27, 0x98, 0x74, - 0xc6, 0xce, 0x30, 0xc0, 0x28, 0x97, 0xa7, 0x88, 0x02, 0x76, 0x51, 0x64, 0x70, 0x46, 0x95, 0xe9, - 0x24, 0x24, 0x65, 0x0a, 0xd9, 0x38, 0x3b, 0x2a, 0x2f, 0xed, 0x79, 0x43, 0x67, 0xe0, 0x7d, 0x5d, - 0xf8, 0x98, 0x32, 0x79, 0xe9, 0x50, 0x00, 0xcd, 0xa8, 0x5c, 0xfb, 0x7c, 0x6a, 0xdc, 0x58, 0x2d, - 0x4b, 0xb0, 0xc4, 0x23, 0x10, 0x30, 0x8f, 0xfc, 0xb6, 0xd1, 0xdc, 0xad, 0x35, 0xf7, 0x55, 0x85, - 0xac, 0x01, 0xb4, 0xcd, 0x56, 0xc5, 0xb0, 0x2c, 0xfa, 0x3b, 0x47, 0x7f, 0x73, 0x77, 0xfd, 0xbd, - 0x6e, 0x5d, 0xcd, 0x4b, 0x1e, 0xfb, 0x05, 0xed, 0xc7, 0x0a, 0x5c, 0xc8, 0x1e, 0x4a, 0xd2, 0x01, - 0x8c, 0xd9, 0xc0, 0xdf, 0xd2, 0x3f, 0x36, 0x77, 0xdc, 0x33, 0xc1, 0xc9, 0xd8, 0x0f, 0x13, 0x16, - 0x53, 0x20, 0x27, 0xde, 0xbe, 0x98, 0x93, 0xa2, 0xd7, 0x37, 0x73, 0x5e, 0x5f, 0xab, 0xc0, 0xd6, - 0x2c, 0x1e, 0xf1, 0xa6, 0xae, 0x43, 0x49, 0x6f, 0xb7, 0xeb, 0xb5, 0x8a, 0xde, 0xa9, 0xb5, 0x9a, - 0xaa, 0x42, 0x96, 0x61, 0x61, 0xdf, 0x6c, 0x75, 0xdb, 0x6a, 0x4e, 0xfb, 0xae, 0x02, 0xab, 0xb5, - 0xc8, 0xea, 0xec, 0xfd, 0x2e, 0xbe, 0x8f, 0xc7, 0x16, 0xdf, 0x56, 0x18, 0xdd, 0x24, 0xfc, 0xc0, - 0xa9, 0x56, 0xde, 0x7b, 0x39, 0xd8, 0x48, 0xd1, 0x10, 0x0b, 0x96, 0xf4, 0x7b, 0x56, 0xab, 0xb6, - 0x5b, 0xe1, 0x35, 0xbb, 0x12, 0x99, 0x4b, 0x61, 0xbe, 0xab, 0xd4, 0x57, 0x98, 0x47, 0xf0, 0xa3, - 0xc0, 0xf6, 0xbd, 0xbe, 0x94, 0xfc, 0xb6, 0x7a, 0xc6, 0x14, 0x9c, 0xf0, 0x24, 0xfb, 0xfa, 0x74, - 0xec, 0x22, 0xdb, 0x5c, 0x4c, 0xaf, 0x1b, 0xc2, 0xd3, 0x8c, 0xd1, 0x7f, 0xc3, 0xa1, 0xe5, 0x69, - 0xd6, 0x11, 0x3f, 0xd2, 0x84, 0xc5, 0x7d, 0x6f, 0x52, 0x9d, 0x3e, 0xe4, 0xeb, 0xf7, 0x72, 0x94, - 0xfd, 0xa8, 0x3a, 0x7d, 0x98, 0x66, 0x8b, 0x2a, 0x4b, 0x16, 0xbd, 0x27, 0xc6, 0x92, 0x73, 0x49, - 0x3a, 0x31, 0x16, 0x9e, 0xca, 0x89, 0x71, 0x67, 0x15, 0x4a, 0xfc, 0x0e, 0x85, 0xd7, 0x93, 0x1f, - 0x2a, 0xb0, 0x35, 0xab, 0xe7, 0xe8, 0xb5, 0x2c, 0x1e, 0xac, 0xe0, 0x42, 0x98, 0x1e, 0x23, 0x1e, - 0xa5, 0x40, 0xa0, 0x91, 0xb7, 0xa0, 0x54, 0x0b, 0x82, 0xa9, 0x3b, 0xb6, 0x5e, 0xe9, 0x9a, 0x35, - 0x3e, 0x5d, 0x9f, 0xfd, 0xe7, 0x77, 0xaf, 0x6c, 0xa2, 0xcf, 0xc7, 0xd8, 0x0e, 0x5e, 0xb1, 0xa7, - 0x63, 0x2f, 0x96, 0x4a, 0x40, 0xa6, 0xa0, 0x52, 0xb4, 0x33, 0xed, 0x7b, 0xae, 0xb8, 0x43, 0x08, - 0x87, 0x6e, 0x0e, 0x93, 0xcf, 0x34, 0x01, 0xd3, 0xbe, 0xa5, 0xc0, 0xf6, 0xec, 0x61, 0xa2, 0xe7, - 0x64, 0x87, 0x99, 0x54, 0x09, 0x97, 0x6a, 0x3c, 0x27, 0x43, 0xbb, 0x2b, 0x99, 0xa7, 0x40, 0xa4, - 0x44, 0x61, 0x6a, 0xfc, 0x5c, 0x2a, 0x1f, 0x76, 0x9c, 0x48, 0x20, 0x6a, 0xf7, 0x61, 0x73, 0xc6, - 0xa0, 0x92, 0x4f, 0x65, 0x26, 0xdd, 0x41, 0x37, 0x25, 0x39, 0xe9, 0x4e, 0x2c, 0x7b, 0x9b, 0x04, - 0xd7, 0xfe, 0x25, 0x07, 0x17, 0xe8, 0xea, 0x1a, 0xb8, 0x41, 0xa0, 0x47, 0xf9, 0x69, 0xe9, 0xae, - 0xf8, 0x1a, 0x2c, 0x1e, 0x3f, 0x9d, 0xaa, 0x98, 0xa1, 0x13, 0x02, 0x78, 0x62, 0x09, 0xe7, 0x18, - 0xfa, 0x3f, 0xb9, 0x0a, 0x72, 0x72, 0xf3, 0x3c, 0x86, 0x37, 0xcd, 0x6d, 0x29, 0xe6, 0xf2, 0x28, - 0xcc, 0x43, 0xfc, 0x3a, 0x2c, 0xa0, 0x3e, 0x85, 0x9f, 0x1d, 0x42, 0xe6, 0xcf, 0xae, 0x1d, 0x6a, - 0x5b, 0x4c, 0x46, 0x40, 0x3e, 0x02, 0x10, 0x65, 0x86, 0xe0, 0x87, 0x83, 0xd0, 0x33, 0x84, 0xc9, - 0x21, 0xcc, 0xe5, 0x93, 0x43, 0x87, 0xa7, 0x5b, 0x28, 0xc3, 0x86, 0xe8, 0xf1, 0x91, 0x88, 0x8a, - 0xc8, 0x5f, 0x31, 0xd7, 0x59, 0x41, 0x6d, 0x24, 0x22, 0x23, 0x5e, 0x4b, 0x25, 0x68, 0xc6, 0xe0, - 0xc8, 0x89, 0x2c, 0xcc, 0xd7, 0x52, 0x59, 0x98, 0x8b, 0x0c, 0x4b, 0x4e, 0xb5, 0xac, 0xfd, 0x43, - 0x0e, 0x96, 0xef, 0x51, 0xa9, 0x0c, 0x75, 0x0d, 0xf3, 0x75, 0x17, 0xb7, 0xa1, 0x54, 0xf7, 0x1d, - 0xfe, 0x5c, 0xc4, 0x7d, 0x4a, 0x98, 0x4f, 0xf7, 0xc0, 0x77, 0xc4, 0xcb, 0x53, 0x60, 0xca, 0x48, - 0x4f, 0xf0, 0x47, 0xbf, 0x03, 0x8b, 0xec, 0xf9, 0x8e, 0xab, 0xd1, 0x84, 0x5c, 0x1e, 0xd6, 0xe8, - 0x65, 0x56, 0x2c, 0xbd, 0x70, 0xb0, 0x27, 0x40, 0x59, 0x48, 0xe4, 0x31, 0x5e, 0x25, 0xcd, 0xca, - 0xc2, 0xe9, 0x34, 0x2b, 0x52, 0x2c, 0xbb, 0xc5, 0xd3, 0xc4, 0xb2, 0xdb, 0x7e, 0x03, 0x4a, 0x52, - 0x7d, 0x9e, 0x4a, 0x4c, 0xff, 0x66, 0x0e, 0x56, 0xb1, 0x55, 0xa1, 0x2d, 0xcf, 0xaf, 0xa6, 0x9e, - 0xe8, 0xe3, 0x31, 0x3d, 0xd1, 0x96, 0x3c, 0x5e, 0xac, 0x65, 0x73, 0x14, 0x44, 0x77, 0x60, 0x23, - 0x85, 0x48, 0x5e, 0x85, 0x05, 0x5a, 0x7d, 0x71, 0xaf, 0x56, 0x93, 0x33, 0x20, 0x8a, 0x7b, 0x4c, - 0x1b, 0x1e, 0x98, 0x0c, 0x5b, 0xfb, 0x77, 0x05, 0x56, 0x78, 0xda, 0x91, 0xe1, 0xa1, 0xff, 0xc4, - 0xee, 0xbc, 0x9e, 0xec, 0x4e, 0x16, 0x5d, 0x85, 0x77, 0xe7, 0x7f, 0x75, 0x27, 0xbe, 0x11, 0xeb, - 0xc4, 0xcd, 0x30, 0x0a, 0xa2, 0x68, 0xce, 0x9c, 0x3e, 0xfc, 0x01, 0xc6, 0x05, 0x8e, 0x23, 0x92, - 0x2f, 0xc2, 0x72, 0xd3, 0x7d, 0x14, 0xbb, 0x9e, 0x5e, 0x9f, 0xc1, 0xf4, 0xe5, 0x10, 0x91, 0xad, - 0x29, 0x3c, 0xd9, 0x87, 0xee, 0x23, 0x3b, 0xf5, 0x72, 0x18, 0xb1, 0xa4, 0x37, 0xd4, 0x38, 0xd9, - 0xd3, 0x4c, 0x7d, 0xee, 0xe0, 0x8a, 0x01, 0x83, 0xbe, 0x9d, 0x07, 0x88, 0x7c, 0x03, 0xe9, 0x02, - 0x8c, 0x19, 0x4d, 0x08, 0xcd, 0x3e, 0x82, 0xe4, 0x39, 0x2e, 0x6c, 0x29, 0xae, 0x73, 0x0d, 0x74, - 0x6e, 0x76, 0x94, 0x4a, 0xd4, 0x45, 0x57, 0xb8, 0x33, 0x5a, 0xdf, 0x1d, 0x38, 0x6c, 0x6f, 0xcf, - 0xef, 0x5c, 0xc3, 0xa0, 0xc4, 0x21, 0x74, 0x46, 0xba, 0x69, 0x74, 0x59, 0xdb, 0xa5, 0x08, 0x29, - 0x7f, 0xdb, 0xc2, 0xd3, 0xf9, 0xdb, 0xb6, 0x61, 0xd9, 0x1b, 0xbe, 0xed, 0x0e, 0x27, 0xfe, 0xf8, - 0x31, 0xaa, 0xdd, 0x23, 0x7d, 0x1e, 0xed, 0x82, 0x9a, 0x28, 0x63, 0xe3, 0x80, 0x67, 0x6e, 0x88, - 0x2f, 0x0f, 0x43, 0x08, 0x0c, 0xfd, 0x85, 0x17, 0xd4, 0xc5, 0x3b, 0x85, 0xe2, 0xa2, 0xba, 0x74, - 0xa7, 0x50, 0x2c, 0xaa, 0xcb, 0x77, 0x0a, 0xc5, 0x65, 0x15, 0x4c, 0xe9, 0xcd, 0x2c, 0x7c, 0x13, - 0x93, 0x9e, 0xb1, 0xe2, 0x4f, 0x54, 0xda, 0xcf, 0x73, 0x40, 0xd2, 0xd5, 0x20, 0x1f, 0x87, 0x12, - 0xdb, 0x60, 0xed, 0x71, 0xf0, 0x55, 0xee, 0x6e, 0xc0, 0xc2, 0x2e, 0x49, 0x60, 0x39, 0xec, 0x12, - 0x03, 0x9b, 0xc1, 0x57, 0x07, 0xe4, 0x0b, 0x70, 0x16, 0xbb, 0x77, 0xe4, 0x8e, 0x3d, 0xbf, 0x6f, - 0x63, 0x8c, 0x5c, 0x67, 0xc0, 0x53, 0x43, 0xbe, 0x84, 0x39, 0x8c, 0xd3, 0xc5, 0x33, 0x86, 0x01, - 0x5d, 0x00, 0xdb, 0x88, 0xd9, 0x66, 0x88, 0xa4, 0x03, 0xaa, 0x4c, 0x7f, 0x38, 0x1d, 0x0c, 0xf8, - 0xc8, 0x96, 0xe9, 0x8d, 0x3e, 0x59, 0x36, 0x83, 0xf1, 0x5a, 0xc4, 0x78, 0x6f, 0x3a, 0x18, 0x90, - 0xd7, 0x00, 0xfc, 0xa1, 0x7d, 0xe2, 0x05, 0x01, 0x7b, 0xcc, 0x09, 0xbd, 0x95, 0x23, 0xa8, 0x3c, - 0x18, 0xfe, 0xb0, 0xc1, 0x80, 0xe4, 0x7f, 0x01, 0x46, 0x6b, 0xc0, 0x30, 0x26, 0xcc, 0x1a, 0x89, - 0x67, 0x6f, 0x11, 0xc0, 0xb8, 0x73, 0xf4, 0x91, 0x6b, 0x79, 0x5f, 0x17, 0xae, 0x1e, 0x9f, 0x83, - 0x0d, 0x6e, 0x3c, 0x7c, 0xcf, 0x9b, 0x1c, 0xf3, 0xab, 0xc4, 0xfb, 0xb9, 0x87, 0x48, 0x77, 0x89, - 0xbf, 0x29, 0x00, 0xe8, 0xf7, 0x2c, 0x11, 0x21, 0xec, 0x26, 0x2c, 0xd0, 0x0b, 0x92, 0x50, 0xb4, - 0xa0, 0x9a, 0x1a, 0xf9, 0xca, 0x6a, 0x6a, 0xc4, 0xa0, 0xab, 0xd1, 0x44, 0xa3, 0x7a, 0xa1, 0x64, - 0xc1, 0xd5, 0xc8, 0xec, 0xec, 0x63, 0x11, 0x9a, 0x39, 0x16, 0xa9, 0x03, 0x44, 0x31, 0xbb, 0xb8, - 0xc8, 0xbf, 0x11, 0x05, 0xbf, 0xe1, 0x05, 0x3c, 0x4b, 0x44, 0x14, 0xf7, 0x4b, 0x9e, 0x3e, 0x11, - 0x1a, 0xb9, 0x0b, 0x85, 0x8e, 0x13, 0xfa, 0xe2, 0xce, 0x88, 0x64, 0xf6, 0x1c, 0x4f, 0xdd, 0x19, - 0x45, 0x33, 0x5b, 0x9b, 0x38, 0xb1, 0x0c, 0xc7, 0xc8, 0x84, 0x18, 0xb0, 0xc8, 0xd3, 0xb2, 0xcf, - 0x88, 0x80, 0xc9, 0xb3, 0xb2, 0xf3, 0xb8, 0xd7, 0x08, 0x94, 0x65, 0x0a, 0x9e, 0x80, 0xfd, 0x36, - 0xe4, 0x2d, 0xab, 0xc1, 0xe3, 0x77, 0xac, 0x46, 0xd7, 0x2f, 0xcb, 0x6a, 0xb0, 0x77, 0xdf, 0x20, - 0x38, 0x91, 0xc8, 0x28, 0x32, 0xf9, 0x04, 0x94, 0x24, 0xa1, 0x98, 0x47, 0xbe, 0xc1, 0x3e, 0x90, - 0xbc, 0x9d, 0xe4, 0x4d, 0x43, 0xc2, 0x26, 0x75, 0x50, 0xef, 0x4e, 0x1f, 0xba, 0xfa, 0x68, 0x84, - 0x6e, 0x90, 0x6f, 0xbb, 0x63, 0x26, 0xb6, 0x15, 0xa3, 0x90, 0xd1, 0xe8, 0x23, 0xd1, 0x17, 0xa5, - 0xb2, 0xb2, 0x29, 0x49, 0x49, 0xda, 0xb0, 0x61, 0xb9, 0x93, 0xe9, 0x88, 0xd9, 0xd7, 0xec, 0xf9, - 0x63, 0x7a, 0xbf, 0x61, 0x71, 0x72, 0x30, 0xba, 0x6e, 0x40, 0x0b, 0x85, 0x51, 0xd3, 0xa1, 0x3f, - 0x4e, 0xdc, 0x75, 0xd2, 0xc4, 0x9a, 0x2b, 0x0f, 0x39, 0x3d, 0x55, 0xe3, 0xb7, 0x26, 0x3c, 0x55, - 0xc5, 0xad, 0x29, 0xba, 0x2b, 0x7d, 0x24, 0x23, 0x96, 0x1b, 0xbe, 0x0c, 0x4a, 0xb1, 0xdc, 0x62, - 0x11, 0xdc, 0xbe, 0x5f, 0x90, 0xc2, 0x89, 0xf2, 0xb1, 0x78, 0x13, 0xe0, 0x8e, 0xef, 0x0d, 0x1b, - 0xee, 0xe4, 0xd8, 0xef, 0x4b, 0x21, 0xe5, 0x4a, 0x5f, 0xf6, 0xbd, 0xa1, 0x7d, 0x82, 0xe0, 0x9f, - 0xbf, 0x7b, 0x45, 0x42, 0x32, 0xa5, 0xff, 0xc9, 0x87, 0x61, 0x99, 0xfe, 0xea, 0x44, 0x56, 0x42, - 0x4c, 0x27, 0x8b, 0xd4, 0x2c, 0xe9, 0x46, 0x84, 0x40, 0xde, 0xc0, 0x34, 0x33, 0xde, 0x68, 0x22, - 0x09, 0xaf, 0x22, 0xa7, 0x8c, 0x37, 0x9a, 0x24, 0x23, 0x44, 0x4b, 0xc8, 0xa4, 0x1a, 0x56, 0x5d, - 0x64, 0x86, 0xe2, 0xd9, 0x6c, 0x50, 0xf1, 0xc8, 0xe7, 0x9a, 0x2d, 0x42, 0xd3, 0xca, 0x29, 0x7f, - 0x13, 0x64, 0x58, 0x09, 0xab, 0xba, 0xcb, 0x5e, 0x8a, 0xb8, 0x50, 0xcb, 0x2a, 0x11, 0x1c, 0xf7, - 0xed, 0x1e, 0x82, 0x63, 0x95, 0x08, 0x91, 0xc9, 0x0e, 0xac, 0x33, 0x19, 0x3f, 0xcc, 0x30, 0xc9, - 0x45, 0x5c, 0xdc, 0xdb, 0xa2, 0x14, 0x94, 0xf2, 0xe7, 0x13, 0x04, 0x64, 0x0f, 0x16, 0xf0, 0xae, - 0xc9, 0x5d, 0x03, 0x2e, 0xca, 0x6a, 0x82, 0xe4, 0x3a, 0xc2, 0x7d, 0x05, 0x15, 0x04, 0xf2, 0xbe, - 0x82, 0xa8, 0xe4, 0xb3, 0x00, 0xc6, 0x70, 0xec, 0x0f, 0x06, 0x18, 0x3c, 0xb9, 0x88, 0x57, 0xa9, - 0x67, 0xe3, 0xeb, 0x11, 0xb9, 0x44, 0x48, 0x3c, 0xd0, 0x1f, 0xfe, 0xb6, 0x13, 0x21, 0x96, 0x25, - 0x5e, 0x5a, 0x0d, 0x16, 0xd9, 0x62, 0xc4, 0x40, 0xe4, 0x3c, 0xb5, 0x8a, 0x14, 0xc6, 0x9a, 0x05, - 0x22, 0xe7, 0xf0, 0x74, 0x20, 0x72, 0x89, 0x40, 0xbb, 0x0b, 0xe7, 0xb2, 0x1a, 0x16, 0xbb, 0x1d, - 0x2b, 0xa7, 0xbd, 0x1d, 0x7f, 0x2f, 0x0f, 0x2b, 0xc8, 0x4d, 0xec, 0xc2, 0x3a, 0xac, 0x5a, 0xd3, - 0x87, 0x61, 0x94, 0x2e, 0xb1, 0x1b, 0x63, 0xfd, 0x02, 0xb9, 0x40, 0x7e, 0xc3, 0x8b, 0x51, 0x10, - 0x03, 0xd6, 0xc4, 0x49, 0xb0, 0x2f, 0x3c, 0x07, 0xc2, 0x18, 0xe0, 0x22, 0xd2, 0x64, 0x3a, 0xc3, - 0x6e, 0x82, 0x28, 0x3a, 0x0f, 0xf2, 0x4f, 0x73, 0x1e, 0x14, 0x4e, 0x75, 0x1e, 0x3c, 0x80, 0x15, - 0xf1, 0x35, 0xdc, 0xc9, 0x17, 0xde, 0xdf, 0x4e, 0x1e, 0x63, 0x46, 0xea, 0xe1, 0x8e, 0xbe, 0x38, - 0x77, 0x47, 0xc7, 0x87, 0x51, 0xb1, 0xca, 0x46, 0x08, 0x4b, 0x6f, 0xec, 0x98, 0x82, 0x72, 0xbf, - 0xd2, 0xfe, 0x05, 0x4e, 0xc9, 0x57, 0x61, 0xb9, 0xee, 0x8b, 0x37, 0x31, 0xe9, 0x31, 0x62, 0x20, - 0x80, 0xb2, 0xb8, 0x10, 0x62, 0x86, 0xa7, 0x5b, 0xfe, 0x83, 0x38, 0xdd, 0xde, 0x00, 0xe0, 0x2e, - 0x29, 0x51, 0xea, 0x38, 0x5c, 0x32, 0x22, 0x42, 0x49, 0xfc, 0x4d, 0x44, 0x42, 0xa6, 0xbb, 0x13, - 0x37, 0xb7, 0xd1, 0x7b, 0x3d, 0x7f, 0x3a, 0x9c, 0xc4, 0x72, 0x2d, 0x0b, 0x0f, 0x56, 0x87, 0x97, - 0xc9, 0xdb, 0x43, 0x82, 0xec, 0x83, 0x1d, 0x10, 0xf2, 0x99, 0xd0, 0xf8, 0x71, 0x69, 0x5e, 0x0f, - 0x69, 0xa9, 0x1e, 0x9a, 0x69, 0xf2, 0xa8, 0xfd, 0x58, 0x91, 0x13, 0x30, 0xfc, 0x02, 0x43, 0xfd, - 0x3a, 0x40, 0x68, 0x94, 0x20, 0xc6, 0x9a, 0xdd, 0x97, 0x42, 0xa8, 0xdc, 0xcb, 0x11, 0xae, 0xd4, - 0x9a, 0xfc, 0x07, 0xd5, 0x9a, 0x0e, 0x94, 0x5a, 0x5f, 0x99, 0x38, 0x91, 0x15, 0x0b, 0x58, 0xa1, - 0x24, 0x8b, 0x3b, 0x53, 0x7e, 0xe7, 0x05, 0x3c, 0x1b, 0x22, 0x39, 0x78, 0x86, 0x08, 0x2c, 0x11, - 0x6a, 0x7f, 0xa6, 0xc0, 0xba, 0xec, 0x76, 0xff, 0x78, 0xd8, 0x23, 0x9f, 0x62, 0xf1, 0x60, 0x95, - 0xd8, 0x95, 0x45, 0x42, 0xa2, 0x5b, 0xee, 0xe3, 0x61, 0x8f, 0x09, 0x40, 0xce, 0x23, 0xb9, 0xb2, - 0x94, 0x90, 0x3c, 0x84, 0x95, 0xb6, 0x3f, 0x18, 0x50, 0xb1, 0x66, 0xfc, 0x36, 0xbf, 0x00, 0x50, - 0x46, 0xc9, 0xa7, 0x11, 0x51, 0xa1, 0x9d, 0xe7, 0xf9, 0x3d, 0x77, 0x73, 0x44, 0xf7, 0x7b, 0x8f, - 0xd3, 0x45, 0x6c, 0xdf, 0x41, 0x3f, 0x39, 0x99, 0xa7, 0xf6, 0x53, 0x05, 0x48, 0xba, 0x4a, 0xf2, - 0x96, 0xa5, 0xfc, 0x37, 0x88, 0xb0, 0x09, 0xd1, 0xaf, 0xf0, 0x34, 0xa2, 0x5f, 0xf9, 0xb7, 0x15, - 0x58, 0xaf, 0xe9, 0x0d, 0x9e, 0x92, 0x81, 0xbd, 0xe0, 0x5c, 0x85, 0x67, 0x6b, 0x7a, 0xc3, 0x6e, - 0xb7, 0xea, 0xb5, 0xca, 0x7d, 0x3b, 0x33, 0xd2, 0xf2, 0xb3, 0xf0, 0x4c, 0x1a, 0x25, 0x7a, 0xe9, - 0xb9, 0x04, 0x5b, 0xe9, 0x62, 0x11, 0x8d, 0x39, 0x9b, 0x58, 0x04, 0x6e, 0xce, 0x97, 0xdf, 0x82, - 0x75, 0x11, 0x79, 0xb8, 0x53, 0xb7, 0x30, 0xb7, 0xc1, 0x3a, 0x94, 0x0e, 0x0c, 0xb3, 0xb6, 0x77, - 0xdf, 0xde, 0xeb, 0xd6, 0xeb, 0xea, 0x19, 0xb2, 0x0a, 0xcb, 0x1c, 0x50, 0xd1, 0x55, 0x85, 0xac, - 0x40, 0xb1, 0xd6, 0xb4, 0x8c, 0x4a, 0xd7, 0x34, 0xd4, 0x5c, 0xf9, 0x2d, 0x58, 0x6b, 0x8f, 0xbd, - 0xb7, 0x9d, 0x89, 0x7b, 0xd7, 0x7d, 0x8c, 0x0f, 0x35, 0x4b, 0x90, 0x37, 0xf5, 0x7b, 0xea, 0x19, - 0x02, 0xb0, 0xd8, 0xbe, 0x5b, 0xb1, 0x6e, 0xdd, 0x52, 0x15, 0x52, 0x82, 0xa5, 0xfd, 0x4a, 0xdb, - 0xbe, 0xdb, 0xb0, 0xd4, 0x1c, 0xfd, 0xa1, 0xdf, 0xb3, 0xf0, 0x47, 0xbe, 0xfc, 0x51, 0xd8, 0x40, - 0x81, 0xa4, 0xee, 0x05, 0x13, 0x77, 0xe8, 0x8e, 0xb1, 0x0e, 0x2b, 0x50, 0xb4, 0x5c, 0xba, 0x93, - 0x4c, 0x5c, 0x56, 0x81, 0xc6, 0x74, 0x30, 0xf1, 0x46, 0x03, 0xf7, 0x6b, 0xaa, 0x52, 0x7e, 0x03, - 0xd6, 0x4d, 0x7f, 0x3a, 0xf1, 0x86, 0x47, 0xd6, 0x84, 0x62, 0x1c, 0x3d, 0x26, 0xe7, 0x61, 0xa3, - 0xdb, 0xd4, 0x1b, 0x3b, 0xb5, 0xfd, 0x6e, 0xab, 0x6b, 0xd9, 0x0d, 0xbd, 0x53, 0xa9, 0xb2, 0x67, - 0xa2, 0x46, 0xcb, 0xea, 0xd8, 0xa6, 0x51, 0x31, 0x9a, 0x1d, 0x55, 0x29, 0x7f, 0x07, 0x75, 0x2b, - 0x3d, 0x7f, 0xd8, 0xdf, 0x73, 0x7a, 0x13, 0x7f, 0x8c, 0x15, 0xd6, 0xe0, 0xb2, 0x65, 0x54, 0x5a, - 0xcd, 0x5d, 0x7b, 0x4f, 0xaf, 0x74, 0x5a, 0x66, 0x56, 0xa8, 0xef, 0x6d, 0xb8, 0x90, 0x81, 0xd3, - 0xea, 0xb4, 0x55, 0x85, 0x5c, 0x81, 0x8b, 0x19, 0x65, 0xf7, 0x8c, 0x1d, 0xbd, 0xdb, 0xa9, 0x36, - 0xd5, 0xdc, 0x0c, 0x62, 0xcb, 0x6a, 0xa9, 0xf9, 0xf2, 0xff, 0x57, 0x60, 0xad, 0x1b, 0x70, 0x93, - 0xf3, 0x2e, 0x7a, 0x9b, 0x3e, 0x07, 0x97, 0xba, 0x96, 0x61, 0xda, 0x9d, 0xd6, 0x5d, 0xa3, 0x69, - 0x77, 0x2d, 0x7d, 0x3f, 0x59, 0x9b, 0x2b, 0x70, 0x51, 0xc2, 0x30, 0x8d, 0x4a, 0xeb, 0xc0, 0x30, - 0xed, 0xb6, 0x6e, 0x59, 0xf7, 0x5a, 0xe6, 0xae, 0xaa, 0xd0, 0x2f, 0x66, 0x20, 0x34, 0xf6, 0x74, - 0x56, 0x9b, 0x58, 0x59, 0xd3, 0xb8, 0xa7, 0xd7, 0xed, 0x9d, 0x56, 0x47, 0xcd, 0x97, 0x1b, 0xf4, - 0x7c, 0xc7, 0x80, 0xbb, 0xcc, 0xb2, 0xb0, 0x08, 0x85, 0x66, 0xab, 0x69, 0x24, 0x1f, 0x17, 0x57, - 0xa0, 0xa8, 0xb7, 0xdb, 0x66, 0xeb, 0x00, 0xa7, 0x18, 0xc0, 0xe2, 0xae, 0xd1, 0xa4, 0x35, 0xcb, - 0xd3, 0x92, 0xb6, 0xd9, 0x6a, 0xb4, 0x3a, 0xc6, 0xae, 0x5a, 0x28, 0x9b, 0x62, 0x09, 0x0b, 0xa6, - 0x3d, 0x9f, 0xbd, 0xe4, 0xed, 0x1a, 0x7b, 0x7a, 0xb7, 0xde, 0xe1, 0x43, 0x74, 0xdf, 0x36, 0x8d, - 0xcf, 0x74, 0x0d, 0xab, 0x63, 0xa9, 0x0a, 0x51, 0x61, 0xa5, 0x69, 0x18, 0xbb, 0x96, 0x6d, 0x1a, - 0x07, 0x35, 0xe3, 0x9e, 0x9a, 0xa3, 0x3c, 0xd9, 0xff, 0xf4, 0x0b, 0xe5, 0xef, 0x2b, 0x40, 0x58, - 0xb0, 0x62, 0x91, 0x01, 0x07, 0x67, 0xcc, 0x65, 0xd8, 0xae, 0xd2, 0xa1, 0xc6, 0xa6, 0x35, 0x5a, - 0xbb, 0xc9, 0x2e, 0xbb, 0x00, 0x24, 0x51, 0xde, 0xda, 0xdb, 0x53, 0x15, 0x72, 0x11, 0xce, 0x26, - 0xe0, 0xbb, 0x66, 0xab, 0xad, 0xe6, 0xb6, 0x73, 0x45, 0x85, 0x6c, 0xa6, 0x0a, 0xef, 0x1a, 0x46, - 0x5b, 0xcd, 0xd3, 0x21, 0x4a, 0x14, 0x88, 0x25, 0xc1, 0xc8, 0x0b, 0xe5, 0x6f, 0x29, 0x70, 0x81, - 0x55, 0x53, 0xac, 0xaf, 0xb0, 0xaa, 0x97, 0x60, 0x8b, 0x87, 0x60, 0xcf, 0xaa, 0xe8, 0x39, 0x50, - 0x63, 0xa5, 0xac, 0x9a, 0xe7, 0x61, 0x23, 0x06, 0xc5, 0x7a, 0xe4, 0xe8, 0xee, 0x11, 0x03, 0xef, - 0x18, 0x56, 0xc7, 0x36, 0xf6, 0xf6, 0x5a, 0x66, 0x87, 0x55, 0x24, 0x5f, 0xd6, 0x60, 0xa3, 0xe2, - 0x8e, 0x27, 0xf4, 0xea, 0x35, 0x0c, 0x3c, 0x7f, 0x88, 0x55, 0x58, 0x85, 0x65, 0xe3, 0xb3, 0x1d, - 0xa3, 0x69, 0xd5, 0x5a, 0x4d, 0xf5, 0x4c, 0xf9, 0x52, 0x02, 0x47, 0xac, 0x63, 0xcb, 0xaa, 0xaa, - 0x67, 0xca, 0x0e, 0xac, 0x0a, 0xc3, 0x6b, 0x36, 0x2b, 0x2e, 0xc3, 0xb6, 0x98, 0x6b, 0xb8, 0xa3, - 0x24, 0x9b, 0xb0, 0x05, 0xe7, 0xd2, 0xe5, 0x46, 0x47, 0x55, 0xe8, 0x28, 0x24, 0x4a, 0x28, 0x3c, - 0x57, 0xfe, 0xbf, 0x0a, 0xac, 0x86, 0x8f, 0x26, 0xa8, 0xa6, 0xbd, 0x02, 0x17, 0x1b, 0x7b, 0xba, - 0xbd, 0x6b, 0x1c, 0xd4, 0x2a, 0x86, 0x7d, 0xb7, 0xd6, 0xdc, 0x4d, 0x7c, 0xe4, 0x19, 0x38, 0x9f, - 0x81, 0x80, 0x5f, 0xd9, 0x82, 0x73, 0xc9, 0xa2, 0x0e, 0x5d, 0xaa, 0x39, 0xda, 0xf5, 0xc9, 0x92, - 0x70, 0x9d, 0xe6, 0xcb, 0x07, 0xb0, 0x66, 0xe9, 0x8d, 0xfa, 0x9e, 0x3f, 0xee, 0xb9, 0xfa, 0x74, - 0x72, 0x3c, 0x24, 0x17, 0x61, 0x73, 0xaf, 0x65, 0x56, 0x0c, 0x1b, 0x51, 0x12, 0x35, 0x38, 0x0b, - 0xeb, 0x72, 0xe1, 0x7d, 0x83, 0x4e, 0x5f, 0x02, 0x6b, 0x32, 0xb0, 0xd9, 0x52, 0x73, 0xe5, 0xcf, - 0xc3, 0x4a, 0x2c, 0x11, 0xde, 0x26, 0x9c, 0x95, 0x7f, 0xb7, 0xdd, 0x61, 0xdf, 0x1b, 0x1e, 0xa9, - 0x67, 0x92, 0x05, 0xe6, 0x74, 0x38, 0xa4, 0x05, 0xb8, 0x9e, 0xe5, 0x82, 0x8e, 0x3b, 0x3e, 0xf1, - 0x86, 0xce, 0xc4, 0xed, 0xab, 0xb9, 0xf2, 0xcb, 0xb0, 0x1a, 0x0b, 0xbf, 0x4d, 0x07, 0xae, 0xde, - 0xe2, 0x1b, 0x70, 0xc3, 0xd8, 0xad, 0x75, 0x1b, 0xea, 0x02, 0x5d, 0xc9, 0xd5, 0xda, 0x7e, 0x55, - 0x85, 0xf2, 0x77, 0x15, 0x7a, 0xcf, 0xc0, 0xa4, 0x3a, 0x8d, 0x3d, 0x5d, 0x0c, 0x35, 0x9d, 0x66, - 0x2c, 0xa8, 0xbf, 0x61, 0x59, 0xec, 0x4d, 0xfd, 0x12, 0x6c, 0xf1, 0x1f, 0xb6, 0xde, 0xdc, 0xb5, - 0xab, 0xba, 0xb9, 0x7b, 0x4f, 0x37, 0xe9, 0xdc, 0xbb, 0xaf, 0xe6, 0x70, 0x41, 0x49, 0x10, 0xbb, - 0xd3, 0xea, 0x56, 0xaa, 0x6a, 0x9e, 0xce, 0xdf, 0x18, 0xbc, 0x5d, 0x6b, 0xaa, 0x05, 0x5c, 0x9e, - 0x29, 0x6c, 0x64, 0x4b, 0xcb, 0x17, 0xca, 0xef, 0x29, 0xb0, 0x69, 0x79, 0x47, 0x43, 0x67, 0x32, - 0x1d, 0xbb, 0xfa, 0xe0, 0xc8, 0x1f, 0x7b, 0x93, 0xe3, 0x13, 0x6b, 0xea, 0x4d, 0x5c, 0x72, 0x13, - 0x5e, 0xb0, 0x6a, 0xfb, 0x4d, 0xbd, 0x43, 0x97, 0x97, 0x5e, 0xdf, 0x6f, 0x99, 0xb5, 0x4e, 0xb5, - 0x61, 0x5b, 0xdd, 0x5a, 0x6a, 0xe6, 0x5d, 0x83, 0xe7, 0x66, 0xa3, 0xd6, 0x8d, 0x7d, 0xbd, 0x72, - 0x5f, 0x55, 0xe6, 0x33, 0xdc, 0xd1, 0xeb, 0x7a, 0xb3, 0x62, 0xec, 0xda, 0x07, 0xb7, 0xd4, 0x1c, - 0x79, 0x01, 0xae, 0xce, 0x46, 0xdd, 0xab, 0xb5, 0x2d, 0x8a, 0x96, 0x9f, 0xff, 0xdd, 0xaa, 0xd5, - 0xa0, 0x58, 0x85, 0xf2, 0x1f, 0x2a, 0xb0, 0x35, 0x2b, 0x06, 0x13, 0xb9, 0x0e, 0x9a, 0xd1, 0xec, - 0x98, 0x7a, 0x6d, 0xd7, 0xae, 0x98, 0xc6, 0xae, 0xd1, 0xec, 0xd4, 0xf4, 0xba, 0x65, 0x5b, 0xad, - 0x2e, 0x9d, 0x4d, 0x91, 0xe9, 0xc3, 0xf3, 0x70, 0x65, 0x0e, 0x5e, 0xab, 0xb6, 0x5b, 0x51, 0x15, - 0x72, 0x0b, 0x5e, 0x9a, 0x83, 0x64, 0xdd, 0xb7, 0x3a, 0x46, 0x43, 0x2e, 0x51, 0x73, 0xe5, 0x0a, - 0x6c, 0xcf, 0x0e, 0xd2, 0x42, 0xb7, 0xe9, 0x78, 0x4f, 0x17, 0xa1, 0xb0, 0x4b, 0x4f, 0x86, 0x58, - 0xee, 0x87, 0xb2, 0x07, 0x6a, 0x32, 0xce, 0x42, 0xca, 0x46, 0xc5, 0xec, 0x36, 0x9b, 0xec, 0x18, - 0x59, 0x87, 0x52, 0xab, 0x53, 0x35, 0x4c, 0x9e, 0x3d, 0x03, 0xd3, 0x65, 0x74, 0x9b, 0x74, 0xe1, - 0xb4, 0xcc, 0xda, 0xe7, 0xf0, 0x3c, 0xd9, 0x82, 0x73, 0x56, 0x5d, 0xaf, 0xdc, 0xb5, 0x9b, 0xad, - 0x8e, 0x5d, 0x6b, 0xda, 0x95, 0xaa, 0xde, 0x6c, 0x1a, 0x75, 0x15, 0xb0, 0x33, 0x67, 0xf9, 0x56, - 0x92, 0x0f, 0xc3, 0x8d, 0xd6, 0xdd, 0x8e, 0x6e, 0xb7, 0xeb, 0xdd, 0xfd, 0x5a, 0xd3, 0xb6, 0xee, - 0x37, 0x2b, 0x42, 0xf6, 0xa9, 0xa4, 0xb7, 0xdc, 0x1b, 0x70, 0x6d, 0x2e, 0x76, 0x94, 0xe7, 0xe2, - 0x3a, 0x68, 0x73, 0x31, 0x79, 0x43, 0xca, 0x3f, 0x51, 0xe0, 0xe2, 0x9c, 0x37, 0x64, 0xf2, 0x12, - 0xdc, 0xac, 0x1a, 0xfa, 0x6e, 0xdd, 0xb0, 0x2c, 0xdc, 0x28, 0xe8, 0x30, 0x30, 0x5b, 0x96, 0xcc, - 0x0d, 0xf5, 0x26, 0xbc, 0x30, 0x1f, 0x3d, 0x3a, 0x9a, 0x6f, 0xc0, 0xb5, 0xf9, 0xa8, 0xfc, 0xa8, - 0xce, 0x91, 0x32, 0x5c, 0x9f, 0x8f, 0x19, 0x1e, 0xf1, 0xf9, 0xf2, 0x6f, 0x2a, 0x70, 0x21, 0x5b, - 0x91, 0x43, 0xeb, 0x56, 0x6b, 0x5a, 0x1d, 0xbd, 0x5e, 0xb7, 0xdb, 0xba, 0xa9, 0x37, 0x6c, 0xa3, - 0x69, 0xb6, 0xea, 0xf5, 0xac, 0xa3, 0xed, 0x1a, 0x3c, 0x37, 0x1b, 0xd5, 0xaa, 0x98, 0xb5, 0x36, - 0xdd, 0xbd, 0x35, 0xb8, 0x3c, 0x1b, 0xcb, 0xa8, 0x55, 0x0c, 0x35, 0xb7, 0xf3, 0xe6, 0x8f, 0xfe, - 0xfe, 0xf2, 0x99, 0x1f, 0xbd, 0x77, 0x59, 0xf9, 0xe9, 0x7b, 0x97, 0x95, 0xbf, 0x7b, 0xef, 0xb2, - 0xf2, 0xb9, 0x17, 0x4f, 0x97, 0x22, 0x0a, 0xe5, 0xfe, 0x87, 0x8b, 0x78, 0x43, 0x79, 0xe5, 0x3f, - 0x03, 0x00, 0x00, 0xff, 0xff, 0x09, 0xed, 0xed, 0x8d, 0x2e, 0xbf, 0x01, 0x00, + 0x76, 0xbb, 0xfd, 0xff, 0xb3, 0xf7, 0x6c, 0x31, 0x6e, 0x5c, 0xd7, 0x69, 0x48, 0xee, 0x2e, 0xf7, + 0x70, 0x1f, 0xb3, 0x57, 0x8f, 0x5d, 0xaf, 0x64, 0xc9, 0x1a, 0xcb, 0x8a, 0x44, 0xc7, 0x4e, 0x24, + 0xd7, 0xb1, 0x9d, 0xc4, 0x71, 0x66, 0xb9, 0xb3, 0x4b, 0x4a, 0x7c, 0x65, 0x86, 0x5c, 0x45, 0x71, + 0x92, 0xc9, 0x88, 0x9c, 0xdd, 0x9d, 0x84, 0xcb, 0x61, 0x38, 0xa4, 0x15, 0x05, 0x05, 0x9a, 0xa0, + 0x40, 0x02, 0xf4, 0x95, 0x36, 0x6d, 0x51, 0x23, 0x3f, 0xf9, 0x68, 0x50, 0xf4, 0xa3, 0xff, 0x2d, + 0x9a, 0xfe, 0xe4, 0x2f, 0x40, 0x10, 0x20, 0x40, 0xff, 0xd2, 0xc2, 0x68, 0x0d, 0xb4, 0x40, 0x1f, + 0x7f, 0x45, 0xfb, 0x11, 0xa0, 0x40, 0x71, 0xcf, 0xbd, 0x77, 0xe6, 0xce, 0x83, 0xd4, 0x2a, 0x76, + 0xda, 0x06, 0xc8, 0xd7, 0x2e, 0xcf, 0x3d, 0xe7, 0xcc, 0x7d, 0xdf, 0x73, 0xcf, 0x3d, 0x0f, 0x77, + 0xc0, 0x77, 0xdc, 0x75, 0x01, 0xaf, 0x30, 0x30, 0xe3, 0xac, 0xbd, 0x01, 0xe7, 0xb2, 0x06, 0x9c, + 0x5c, 0x85, 0x15, 0x39, 0x18, 0x10, 0x67, 0x52, 0x72, 0x46, 0x9e, 0x08, 0x07, 0xc4, 0x19, 0x7c, + 0x5f, 0x81, 0x4b, 0xf3, 0xb6, 0x0f, 0xb2, 0x0d, 0xc5, 0xd1, 0xd8, 0xf3, 0x51, 0x4c, 0xe5, 0xd9, + 0x16, 0xc4, 0x6f, 0x4c, 0xa4, 0x80, 0xf2, 0xd4, 0xc4, 0x39, 0xe2, 0x0e, 0x1e, 0xe6, 0x32, 0x42, + 0x3a, 0xce, 0x51, 0x40, 0x9e, 0x87, 0x8d, 0xbe, 0x7b, 0xe8, 0x4c, 0x07, 0x13, 0x3b, 0xe8, 0x1d, + 0xbb, 0x7d, 0x74, 0xc1, 0x42, 0xc3, 0x3d, 0x53, 0xe5, 0x05, 0x96, 0x80, 0xa7, 0x6a, 0xbc, 0x30, + 0xa3, 0xc6, 0x77, 0x0a, 0x45, 0x45, 0xcd, 0x99, 0x68, 0x29, 0xa5, 0x7d, 0x2d, 0x07, 0x5b, 0xb3, + 0xd6, 0x0b, 0x79, 0x3d, 0xab, 0x0f, 0xd8, 0xc3, 0x85, 0x0c, 0x97, 0x1f, 0x2e, 0xa4, 0xaf, 0x91, + 0xdb, 0x10, 0x3a, 0x50, 0x3d, 0x2e, 0x18, 0x82, 0x80, 0x51, 0x9a, 0x91, 0x13, 0x04, 0x0f, 0xe9, + 0x96, 0x90, 0x97, 0x02, 0xea, 0x72, 0x98, 0x4c, 0x23, 0x60, 0xe4, 0x15, 0x80, 0xde, 0xc0, 0x0f, + 0x5c, 0xb4, 0x0f, 0xe0, 0xb2, 0x06, 0x33, 0x0b, 0x0f, 0xa1, 0xf2, 0x83, 0x30, 0x42, 0x2b, 0x7e, + 0xdf, 0xe5, 0x03, 0xe8, 0xc0, 0xe6, 0x8c, 0x0d, 0x92, 0x0e, 0x4f, 0x94, 0x9d, 0x5e, 0xe4, 0xba, + 0x9a, 0x86, 0x39, 0xea, 0x93, 0x3d, 0x9e, 0x9b, 0x35, 0x47, 0x1e, 0x01, 0x49, 0xef, 0x82, 0x94, + 0x3b, 0x37, 0x6e, 0x9e, 0x8e, 0x43, 0xee, 0x0c, 0xd2, 0x1d, 0x0f, 0xc8, 0x15, 0x28, 0x89, 0x5c, + 0x96, 0x54, 0x96, 0x67, 0xcc, 0x81, 0x83, 0xee, 0xba, 0x38, 0x79, 0x30, 0x62, 0x2a, 0xba, 0xc9, + 0x71, 0x29, 0x61, 0x19, 0x21, 0x9d, 0x47, 0x23, 0xd1, 0xba, 0x4b, 0x62, 0x7e, 0xc7, 0xcf, 0x26, + 0x5e, 0xfa, 0xc7, 0x8a, 0x18, 0xfe, 0xf4, 0xe6, 0xfe, 0xb8, 0xfa, 0x11, 0x40, 0x2f, 0x25, 0x5e, + 0x31, 0xfc, 0x9f, 0x4a, 0x2d, 0x62, 0xd5, 0x71, 0xa9, 0x85, 0xff, 0x24, 0xd7, 0x61, 0x7d, 0xcc, + 0xec, 0x58, 0x27, 0x3e, 0xef, 0x4f, 0x96, 0x37, 0x64, 0x95, 0x81, 0x3b, 0x3e, 0xf6, 0x29, 0xaf, + 0xd7, 0x9d, 0xb0, 0xc3, 0xa4, 0xb3, 0x8e, 0xbc, 0x08, 0xcb, 0xf4, 0xac, 0xc3, 0x48, 0x3b, 0x09, + 0xf7, 0x08, 0xc4, 0x43, 0xc9, 0xc1, 0x2c, 0x7e, 0x91, 0xff, 0xcf, 0x79, 0xbd, 0x9d, 0x13, 0xcc, + 0xe4, 0x93, 0x96, 0x6c, 0xc2, 0x92, 0x3f, 0x3e, 0x92, 0x9a, 0xb6, 0xe8, 0x8f, 0x8f, 0x68, 0xbb, + 0x6e, 0x80, 0xca, 0xbc, 0x75, 0x58, 0xd4, 0x84, 0xe0, 0xd1, 0x90, 0x5d, 0xc5, 0x8b, 0xe6, 0x1a, + 0x83, 0x63, 0xc2, 0xfe, 0x47, 0xc3, 0x1e, 0xc5, 0x0c, 0x02, 0xdf, 0x96, 0x03, 0x6c, 0xf1, 0x66, + 0xaf, 0x05, 0x81, 0x1f, 0x45, 0xda, 0xea, 0x93, 0x1d, 0x58, 0xa5, 0x7c, 0xc2, 0x30, 0x5f, 0x5c, + 0x10, 0x78, 0x3a, 0x2d, 0x08, 0x3c, 0x1a, 0xf6, 0x44, 0x15, 0xcd, 0x95, 0x40, 0xfa, 0x45, 0xee, + 0x82, 0x2a, 0x49, 0x4c, 0xe8, 0xbe, 0x99, 0xb0, 0xa9, 0x8e, 0xd8, 0x48, 0x92, 0x56, 0x6d, 0x78, + 0xe8, 0x9b, 0xeb, 0xbd, 0x38, 0x80, 0x77, 0xcd, 0x77, 0x15, 0xb1, 0x97, 0x66, 0x10, 0x11, 0x0d, + 0x56, 0x8f, 0x9d, 0xc0, 0x0e, 0x82, 0x13, 0x66, 0x23, 0xc6, 0x03, 0x0b, 0x97, 0x8e, 0x9d, 0xc0, + 0x0a, 0x4e, 0x44, 0xe2, 0x92, 0xf3, 0x14, 0xc7, 0x77, 0xa6, 0x93, 0x63, 0x5b, 0x96, 0xff, 0x58, + 0x8f, 0x9d, 0x3d, 0x76, 0x82, 0x16, 0x2d, 0x93, 0x78, 0x93, 0x6b, 0xb0, 0x86, 0x7c, 0x7b, 0x9e, + 0x60, 0x8c, 0x91, 0x2f, 0xcc, 0x15, 0xca, 0xb8, 0xe7, 0x31, 0xce, 0xbc, 0x86, 0xff, 0x92, 0x83, + 0x0b, 0xd9, 0xbd, 0x83, 0xd3, 0x93, 0xf6, 0x29, 0xfa, 0xe8, 0xf1, 0xba, 0x2d, 0x53, 0x08, 0x8b, + 0x5a, 0x92, 0x35, 0x38, 0xb9, 0xcc, 0xc1, 0x29, 0xc3, 0x06, 0x32, 0xe2, 0x92, 0xe6, 0xc0, 0x0b, + 0x26, 0x3c, 0x18, 0x87, 0xb9, 0x4e, 0x0b, 0xd8, 0x7e, 0x5e, 0xa7, 0x60, 0xf2, 0x1c, 0xac, 0x89, + 0x1d, 0xd9, 0x7f, 0x38, 0xa4, 0x1f, 0x66, 0xdb, 0xf1, 0x2a, 0x87, 0xb6, 0x10, 0x48, 0xce, 0xc3, + 0xa2, 0x33, 0x1a, 0xd1, 0x4f, 0xb2, 0x5d, 0x78, 0xc1, 0x19, 0x8d, 0x58, 0x72, 0x1d, 0xf4, 0x48, + 0xb4, 0x0f, 0xd1, 0x4a, 0x88, 0x9b, 0x24, 0x9a, 0x2b, 0x08, 0x64, 0x96, 0x43, 0x01, 0x5d, 0xf7, + 0x94, 0x56, 0xa0, 0x2c, 0x21, 0x0a, 0x38, 0xa3, 0x10, 0xe1, 0x29, 0x28, 0x8a, 0xf7, 0x6a, 0xe6, + 0x58, 0x61, 0x2e, 0x39, 0xfc, 0xad, 0xfa, 0x65, 0xd8, 0xec, 0x7b, 0x01, 0x4e, 0x5e, 0xd6, 0xa4, + 0xd1, 0x88, 0xfb, 0x40, 0xb2, 0x20, 0xbd, 0xe6, 0x39, 0x5e, 0x4c, 0x7b, 0x52, 0x1f, 0x8d, 0x98, + 0x27, 0x24, 0xef, 0xeb, 0x57, 0x61, 0x9d, 0x4b, 0x5c, 0xfc, 0x88, 0xc4, 0xba, 0xf0, 0x05, 0x4c, + 0xaf, 0x42, 0x3c, 0x9d, 0x11, 0x70, 0x50, 0xad, 0x2f, 0x28, 0xff, 0x5e, 0x81, 0xf3, 0x99, 0x22, + 0x1b, 0xf9, 0x02, 0x30, 0x97, 0xaf, 0x89, 0x6f, 0x8f, 0xdd, 0x9e, 0x37, 0xf2, 0x30, 0x86, 0x06, + 0x53, 0x69, 0xde, 0x9e, 0x27, 0xec, 0xa1, 0xfb, 0x58, 0xc7, 0x37, 0x43, 0x22, 0xa6, 0x6b, 0x51, + 0xc7, 0x09, 0xf0, 0xf6, 0x9b, 0x70, 0x3e, 0x13, 0x35, 0x43, 0x07, 0xf2, 0xc1, 0x78, 0x32, 0x69, + 0xf1, 0x48, 0x95, 0x68, 0xb4, 0xa4, 0x1b, 0xe1, 0xcd, 0xfb, 0x41, 0xd8, 0xbc, 0x84, 0x70, 0x47, + 0x8c, 0xe4, 0xba, 0xce, 0xba, 0x9f, 0x08, 0xa2, 0xd9, 0x4b, 0xfb, 0x4d, 0x38, 0xcf, 0x27, 0xdf, + 0xd1, 0xd8, 0x19, 0x1d, 0x47, 0xec, 0x58, 0x45, 0x3f, 0x90, 0xc5, 0x8e, 0xcd, 0xca, 0x7d, 0x8a, + 0x1f, 0x72, 0x3d, 0xeb, 0xa4, 0x81, 0xbc, 0x0d, 0x5f, 0xcf, 0x89, 0xa5, 0x9e, 0x51, 0x9d, 0x8c, + 0x69, 0xad, 0x64, 0x4d, 0xeb, 0xd3, 0xaf, 0xa9, 0x26, 0x10, 0x79, 0xb3, 0x62, 0x5a, 0x4f, 0x6e, + 0x50, 0x25, 0xe4, 0x74, 0x5e, 0x11, 0x69, 0x6b, 0xb0, 0x58, 0x32, 0xcf, 0x8d, 0x5e, 0x12, 0x44, + 0x2e, 0xc2, 0x72, 0x98, 0x2f, 0x9b, 0x1f, 0x1c, 0x45, 0x06, 0xa8, 0xf5, 0xc9, 0x33, 0xb0, 0xc2, + 0x44, 0xf2, 0xd8, 0x9a, 0x03, 0x84, 0xe9, 0x74, 0xe1, 0x89, 0x3e, 0x50, 0xe0, 0x99, 0xc7, 0xf5, + 0x21, 0xb9, 0x07, 0x17, 0xd0, 0xac, 0x23, 0xf0, 0xc3, 0x61, 0xb0, 0x7b, 0x4e, 0xef, 0xd8, 0xe5, + 0xb3, 0x56, 0xcb, 0x1c, 0x8c, 0xd1, 0xc8, 0xb2, 0x5a, 0xd2, 0x38, 0x8c, 0x46, 0x56, 0xe0, 0x8b, + 0xdf, 0x15, 0x4a, 0xce, 0xeb, 0xd0, 0x87, 0x8b, 0x73, 0x28, 0xa5, 0x8d, 0x43, 0x91, 0x37, 0x8e, + 0x1b, 0xa0, 0x1e, 0xba, 0x7d, 0x2a, 0x13, 0xbb, 0x7d, 0xac, 0xda, 0x5b, 0xb7, 0x59, 0x86, 0x78, + 0x73, 0x2d, 0x84, 0x5b, 0x81, 0x7f, 0x70, 0x9b, 0x7f, 0xe5, 0x44, 0x1c, 0x79, 0xf2, 0xb5, 0x82, + 0xbc, 0x08, 0x67, 0x13, 0xf1, 0x49, 0x22, 0x87, 0x77, 0x73, 0x83, 0x16, 0xc5, 0xa3, 0x59, 0x5d, + 0x85, 0x15, 0x31, 0x2b, 0xc6, 0xa1, 0x1f, 0x9c, 0x59, 0xe2, 0x30, 0xba, 0xea, 0xf8, 0xe7, 0xa6, + 0xa2, 0x51, 0x99, 0x37, 0x92, 0x53, 0xc8, 0xd2, 0xe4, 0x05, 0x20, 0xa1, 0xdc, 0x1e, 0x6e, 0x14, + 0xfc, 0x83, 0x1b, 0xa2, 0x24, 0x5c, 0xe1, 0xfc, 0xb3, 0x7f, 0x93, 0x83, 0xb3, 0x19, 0x57, 0x19, + 0x7a, 0x09, 0xf0, 0x86, 0x13, 0xf7, 0x88, 0x5d, 0x21, 0xe4, 0x46, 0xae, 0x4b, 0x70, 0xae, 0x9f, + 0x5a, 0x64, 0x19, 0xd0, 0xf9, 0xb7, 0xf8, 0x2f, 0xba, 0x79, 0x38, 0x63, 0xa1, 0x7a, 0xa1, 0xff, + 0x92, 0x1a, 0x6c, 0x60, 0x5a, 0x87, 0xc0, 0xf3, 0x31, 0x3b, 0x04, 0x0a, 0x21, 0x85, 0xd8, 0x65, + 0x07, 0x6b, 0xd1, 0x96, 0x90, 0xa8, 0x14, 0x62, 0xaa, 0xa3, 0x04, 0x84, 0x7c, 0x0c, 0xb6, 0xa5, + 0xb3, 0xc6, 0x4e, 0xac, 0x3c, 0xb4, 0x74, 0x37, 0x37, 0x9d, 0xf0, 0xd4, 0xd9, 0x8d, 0xad, 0xc1, + 0x1d, 0xb8, 0x8c, 0x83, 0xe8, 0xf5, 0x47, 0x76, 0x2a, 0x0f, 0x08, 0x36, 0x95, 0x05, 0xce, 0xdf, + 0xa6, 0x58, 0xb5, 0xfe, 0x28, 0x91, 0x12, 0x84, 0xb6, 0x9a, 0x77, 0xdf, 0x9b, 0x70, 0x3e, 0xb3, + 0xc6, 0xf4, 0x80, 0x41, 0x43, 0xaa, 0x48, 0x36, 0x5a, 0xa2, 0xbf, 0xa9, 0x70, 0x74, 0x15, 0x56, + 0x1e, 0xb8, 0xce, 0xd8, 0x1d, 0xf3, 0x93, 0x9b, 0x4f, 0x09, 0x06, 0x93, 0x0f, 0xee, 0x7e, 0x7c, + 0x68, 0xb8, 0xce, 0x88, 0x34, 0xe0, 0x2c, 0x3b, 0x01, 0xbd, 0x13, 0x14, 0x06, 0xb9, 0x9e, 0x49, + 0x89, 0x89, 0x43, 0x48, 0x82, 0x47, 0x53, 0x0d, 0xb1, 0x18, 0xb5, 0xb9, 0x71, 0x94, 0x04, 0xd1, + 0x15, 0x7d, 0x21, 0x1b, 0x9b, 0xec, 0x40, 0x89, 0x31, 0x67, 0xd7, 0x02, 0xf6, 0x40, 0x70, 0x75, + 0xee, 0x17, 0x2a, 0x68, 0x5f, 0x1c, 0x84, 0xff, 0xd3, 0xf3, 0x1a, 0xdf, 0x62, 0xed, 0x13, 0xf9, + 0xfd, 0xc3, 0x5c, 0x41, 0x20, 0x7f, 0xf7, 0xd0, 0xfe, 0x56, 0x11, 0x4d, 0x8d, 0x5d, 0x8e, 0xe9, + 0xd4, 0x0a, 0xdc, 0xa1, 0x78, 0x03, 0x5a, 0x36, 0xf9, 0xaf, 0x27, 0x9c, 0xea, 0xe4, 0x15, 0x58, + 0xa1, 0x6c, 0x8f, 0xa6, 0x43, 0x36, 0xe5, 0xf2, 0xb1, 0xb8, 0x3c, 0x0d, 0x56, 0x44, 0x87, 0xad, + 0x7a, 0xc6, 0x2c, 0x9d, 0x44, 0x3f, 0xa9, 0xb4, 0x1c, 0x9c, 0x4c, 0x46, 0xf2, 0x44, 0x15, 0x8a, + 0x42, 0xab, 0xd1, 0x69, 0x73, 0x92, 0x22, 0xc5, 0x89, 0xa4, 0xe5, 0x9d, 0x45, 0xa6, 0x2a, 0xd4, + 0x9e, 0x87, 0x92, 0xc4, 0x9b, 0x36, 0x86, 0x79, 0xce, 0x88, 0xc6, 0xb0, 0x5f, 0x7c, 0xb0, 0x1f, + 0x40, 0x51, 0xb0, 0xa4, 0xd7, 0x82, 0x63, 0x3f, 0x10, 0x8b, 0x1c, 0xff, 0xa7, 0x30, 0xda, 0xcb, + 0xd8, 0xc8, 0x05, 0x13, 0xff, 0xc7, 0xb3, 0x64, 0xe2, 0xd0, 0xfb, 0xc0, 0x20, 0xb0, 0x47, 0x68, + 0x81, 0x15, 0x0a, 0xcf, 0x14, 0xde, 0x19, 0x04, 0xcc, 0x2e, 0x8b, 0x7f, 0xe3, 0xaf, 0xc2, 0x43, + 0x38, 0xa1, 0x4d, 0x98, 0xb5, 0x67, 0xc6, 0x8e, 0x8c, 0x5c, 0xfa, 0xc8, 0x60, 0xf1, 0x56, 0x38, + 0x25, 0xfb, 0x32, 0x20, 0x0c, 0x8f, 0x0c, 0x69, 0x67, 0x28, 0xc4, 0x76, 0x06, 0xe9, 0x4e, 0x1e, + 0x8d, 0x1e, 0x3b, 0x71, 0xc4, 0x9d, 0x3c, 0xb9, 0x4f, 0xfd, 0x59, 0x4e, 0xa8, 0x08, 0x76, 0x7c, + 0x7f, 0x12, 0x4c, 0xc6, 0xce, 0x28, 0xa6, 0x0a, 0x25, 0x27, 0xf0, 0x14, 0x4a, 0xd0, 0xb7, 0x31, + 0x85, 0x86, 0x3f, 0x16, 0x21, 0x3e, 0xc2, 0x99, 0x5b, 0xba, 0xfd, 0xa1, 0xb8, 0x8c, 0xaf, 0x53, + 0x6c, 0x5d, 0x46, 0xa6, 0x13, 0x56, 0xe2, 0x5a, 0x3d, 0x63, 0x6e, 0x32, 0x9e, 0x29, 0x2c, 0x52, + 0xcd, 0x58, 0xc4, 0x49, 0x5d, 0xe8, 0x4e, 0xb4, 0xa2, 0xe3, 0x5c, 0xe5, 0xb5, 0x4e, 0x3e, 0x01, + 0xcb, 0x5e, 0x5f, 0xce, 0x14, 0x99, 0xd4, 0xc2, 0xd5, 0xfa, 0x2c, 0x5a, 0x75, 0xc4, 0x83, 0xce, + 0x39, 0x8f, 0x43, 0x77, 0x56, 0x63, 0x4a, 0x63, 0x6d, 0x47, 0xdc, 0x46, 0xd3, 0x64, 0x64, 0x0d, + 0x72, 0xe1, 0x08, 0xe7, 0xbc, 0x3e, 0x5b, 0x5e, 0x51, 0xbc, 0x6c, 0x93, 0xff, 0xd2, 0x7e, 0x1d, + 0x6e, 0x9c, 0xb6, 0x8f, 0xe8, 0x52, 0x9c, 0xd1, 0xe1, 0xcb, 0x2c, 0x54, 0x65, 0xbc, 0xdf, 0xae, + 0x82, 0x1c, 0xee, 0xd7, 0x13, 0x9b, 0x9f, 0x80, 0x75, 0xc7, 0x9e, 0xf6, 0x97, 0x79, 0x58, 0x8b, + 0xab, 0xc9, 0xc9, 0xf3, 0x50, 0x90, 0x76, 0xa0, 0xcd, 0x0c, 0x5d, 0x3a, 0xee, 0x3b, 0x88, 0x74, + 0xaa, 0x1d, 0x87, 0xdc, 0x81, 0x35, 0x34, 0xdc, 0x43, 0xd1, 0x73, 0xe2, 0xf1, 0xc7, 0x97, 0xf9, + 0xef, 0x67, 0xc5, 0x1f, 0xbe, 0x73, 0xe5, 0x0c, 0x3e, 0x95, 0xad, 0x50, 0x5a, 0x2a, 0xfd, 0xd1, + 0x42, 0x49, 0x0b, 0x5a, 0x98, 0xad, 0x05, 0xe5, 0x4d, 0x99, 0xa1, 0x05, 0x5d, 0x98, 0xa3, 0x05, + 0x8d, 0x28, 0x65, 0x2d, 0x28, 0xea, 0xc2, 0x97, 0x66, 0xe9, 0xc2, 0x23, 0x1a, 0xa6, 0x0b, 0x8f, + 0xb4, 0x98, 0xc5, 0x99, 0x5a, 0xcc, 0x88, 0x86, 0x6b, 0x31, 0xaf, 0xf1, 0x3e, 0x1a, 0x3b, 0x0f, + 0x6d, 0xec, 0x3c, 0x7e, 0x2c, 0x62, 0xeb, 0x4d, 0xe7, 0x21, 0x1a, 0xd7, 0xec, 0x2c, 0x83, 0xb0, + 0xc8, 0xd1, 0xfe, 0x50, 0x49, 0x68, 0x02, 0xc5, 0xf8, 0x3d, 0x07, 0x6b, 0xec, 0xb0, 0xe2, 0xe1, + 0x4c, 0xd9, 0x69, 0xb5, 0x6a, 0xae, 0x0a, 0x28, 0xbb, 0x6f, 0x7e, 0x00, 0xd6, 0x43, 0x34, 0x7e, + 0xe5, 0x42, 0x4f, 0x3d, 0x33, 0xa4, 0xe6, 0x61, 0x67, 0x9e, 0x87, 0x8d, 0x10, 0x91, 0x6b, 0x73, + 0xd8, 0x75, 0x73, 0xd5, 0x54, 0x45, 0x41, 0x9b, 0xc3, 0xb5, 0xa3, 0xe4, 0xcd, 0xe3, 0x17, 0x54, + 0x2b, 0xed, 0x07, 0xf9, 0x98, 0x96, 0x44, 0x7c, 0x86, 0x9e, 0xa2, 0x81, 0x6f, 0xf3, 0x4e, 0xe2, + 0x7b, 0xd1, 0xd5, 0x19, 0x63, 0xc6, 0x6d, 0x9a, 0x2c, 0xab, 0x65, 0x42, 0x10, 0xf8, 0xc2, 0xc4, + 0xc9, 0x66, 0x12, 0x35, 0x3b, 0xf7, 0x71, 0xce, 0x0a, 0x76, 0x6c, 0xe3, 0x29, 0xcf, 0x67, 0x27, + 0xae, 0xa9, 0x74, 0xca, 0xa2, 0x64, 0x1d, 0xfe, 0x12, 0x1f, 0xe8, 0x02, 0x2a, 0x15, 0x83, 0x38, + 0xf3, 0x7c, 0xc6, 0xdd, 0x29, 0xc5, 0x1c, 0x7b, 0x09, 0x39, 0xab, 0x53, 0xf1, 0xaf, 0x60, 0x6b, + 0xc0, 0x0a, 0xea, 0x28, 0x04, 0xc3, 0x42, 0x86, 0x0a, 0x3e, 0xdd, 0xf8, 0x4a, 0xad, 0x61, 0x96, + 0x28, 0x9d, 0x60, 0x73, 0x0c, 0x4f, 0xc9, 0x9a, 0x85, 0x78, 0x25, 0x17, 0x44, 0x14, 0xdf, 0xb9, + 0x3d, 0x10, 0x29, 0x20, 0xb0, 0xaa, 0x17, 0x9c, 0x38, 0x80, 0xa3, 0x69, 0xc7, 0xb0, 0x3d, 0x7b, + 0x48, 0xe6, 0x64, 0x88, 0x8a, 0x0e, 0xd0, 0x9c, 0x7c, 0x80, 0xca, 0x7a, 0x86, 0x7c, 0x4c, 0xcf, + 0xa0, 0xfd, 0x69, 0x1e, 0x9e, 0x3d, 0xc5, 0x70, 0xcd, 0xf9, 0xe6, 0x27, 0xe3, 0xe2, 0x59, 0x2e, + 0x76, 0x33, 0xa4, 0x4c, 0xf9, 0x06, 0x49, 0x6f, 0xa9, 0xd9, 0xc2, 0xd9, 0x17, 0x60, 0x9d, 0xed, + 0x82, 0xcc, 0x2c, 0xf1, 0x70, 0x3a, 0x38, 0xc5, 0x36, 0x78, 0x51, 0xf8, 0x50, 0x25, 0x48, 0x71, + 0x67, 0xc4, 0x1d, 0xc3, 0x0a, 0x61, 0xa4, 0x03, 0x25, 0x44, 0x3b, 0x74, 0xbc, 0xc1, 0xa9, 0x9c, + 0x79, 0x84, 0x87, 0x96, 0x4c, 0xc6, 0xac, 0xa9, 0x29, 0x60, 0x0f, 0x7f, 0x93, 0xeb, 0xb0, 0x3e, + 0x9c, 0x9e, 0x50, 0xc1, 0x83, 0xcd, 0x05, 0x6e, 0xfd, 0xb1, 0x60, 0xae, 0x0e, 0xa7, 0x27, 0xfa, + 0x68, 0x84, 0x43, 0x8a, 0x66, 0x22, 0x1b, 0x14, 0x8f, 0xad, 0x5a, 0x81, 0xb9, 0x88, 0x98, 0x94, + 0x01, 0x5b, 0xb7, 0x1c, 0xf7, 0x1c, 0x30, 0xa3, 0x41, 0x9e, 0x21, 0x8b, 0xfd, 0xd0, 0xfe, 0x2b, + 0x27, 0xee, 0xbb, 0xb3, 0xe7, 0xfd, 0xaf, 0x86, 0x28, 0x63, 0x88, 0x6e, 0x80, 0x4a, 0xbb, 0x3e, + 0xda, 0x54, 0xc2, 0x31, 0x5a, 0x1b, 0x4e, 0x4f, 0xc2, 0xbe, 0x93, 0x3b, 0x7e, 0x51, 0xee, 0xf8, + 0x57, 0xc4, 0x7d, 0x38, 0x73, 0x7b, 0x98, 0xdd, 0xe5, 0xda, 0xbf, 0xe7, 0xe1, 0xfa, 0xe9, 0x36, + 0x81, 0x5f, 0x8d, 0x5b, 0xc6, 0xb8, 0x25, 0x54, 0xa7, 0x0b, 0x29, 0xd5, 0x69, 0xc6, 0xda, 0x5b, + 0xcc, 0x5a, 0x7b, 0x29, 0x45, 0xed, 0x52, 0x86, 0xa2, 0x36, 0x73, 0x81, 0x16, 0x1f, 0xb3, 0x40, + 0x97, 0xe5, 0x79, 0xf2, 0xcf, 0xa1, 0x02, 0x23, 0x7e, 0x1f, 0x78, 0x13, 0xce, 0x8a, 0xfb, 0x00, + 0x3b, 0x39, 0x22, 0xfd, 0x7b, 0xe9, 0xf6, 0xcd, 0xac, 0x9b, 0x00, 0xa2, 0x65, 0x48, 0xeb, 0x1b, + 0xfc, 0x0e, 0x10, 0x95, 0xff, 0xff, 0x91, 0xfe, 0xc9, 0x7d, 0xb8, 0x80, 0xf1, 0xe5, 0x7b, 0xf2, + 0xcb, 0x81, 0x3d, 0x76, 0x0f, 0xf9, 0x7c, 0xb8, 0x9a, 0x92, 0x95, 0xbd, 0x9e, 0x54, 0x1d, 0xd3, + 0x3d, 0xac, 0x9e, 0x31, 0xcf, 0x05, 0x19, 0xf0, 0xe4, 0xc5, 0xe2, 0x2f, 0x14, 0xd0, 0x1e, 0xdf, + 0x5f, 0xa8, 0xa8, 0x4a, 0x76, 0xf8, 0xb2, 0x59, 0x72, 0xa4, 0xde, 0x7b, 0x16, 0x56, 0xc7, 0xee, + 0xe1, 0xd8, 0x0d, 0x8e, 0x63, 0x1a, 0x90, 0x15, 0x0e, 0x14, 0x1d, 0x23, 0x82, 0x52, 0x3e, 0x91, + 0x64, 0x2e, 0x88, 0xb4, 0xbd, 0xf0, 0xbe, 0x98, 0x39, 0x0e, 0x74, 0x36, 0xc9, 0x15, 0x64, 0x3f, + 0xee, 0x14, 0x8a, 0x39, 0x35, 0x6f, 0xf2, 0xd0, 0x99, 0x87, 0xde, 0xc0, 0xd5, 0xfe, 0x5a, 0x11, + 0x12, 0x41, 0x56, 0xe7, 0x91, 0x37, 0x25, 0x63, 0xde, 0x7c, 0x4a, 0x0c, 0xc9, 0x22, 0x91, 0xed, + 0x1e, 0x79, 0x78, 0x46, 0x04, 0xc4, 0xc2, 0x33, 0x22, 0xe4, 0x3d, 0x58, 0x24, 0xf2, 0x5b, 0xf3, + 0x6b, 0xc2, 0x22, 0x88, 0xee, 0x79, 0x07, 0xb7, 0xc8, 0x4d, 0x58, 0x62, 0x46, 0x40, 0xa2, 0xba, + 0xeb, 0xb1, 0xea, 0x1e, 0xdc, 0x32, 0x45, 0xb9, 0xf6, 0x76, 0xf8, 0xae, 0x95, 0x6a, 0xc4, 0xc1, + 0x2d, 0xf2, 0xca, 0xe9, 0x8c, 0x73, 0x8b, 0xc2, 0x38, 0x37, 0x34, 0xcc, 0x7d, 0x35, 0x66, 0x98, + 0x7b, 0x6d, 0x7e, 0x6f, 0xf1, 0xd7, 0x48, 0x16, 0x8e, 0x30, 0x0a, 0x53, 0xf5, 0xd3, 0x1c, 0x3c, + 0x3d, 0x97, 0x82, 0x5c, 0x82, 0xa2, 0xde, 0xae, 0x75, 0xa2, 0xf1, 0xa5, 0x6b, 0x46, 0x40, 0xc8, + 0x3e, 0x2c, 0xef, 0x38, 0x81, 0xd7, 0xa3, 0xd3, 0x38, 0xf3, 0x79, 0x20, 0xc5, 0x36, 0x44, 0xaf, + 0x9e, 0x31, 0x23, 0x5a, 0x62, 0xc3, 0x06, 0xae, 0x85, 0x58, 0xea, 0xa9, 0x7c, 0x86, 0xae, 0x21, + 0xc5, 0x30, 0x45, 0x46, 0xf7, 0x99, 0x14, 0x90, 0x3c, 0x00, 0x62, 0x59, 0xd5, 0x8a, 0x3b, 0x9e, + 0xf0, 0x3b, 0xf8, 0xc4, 0x0b, 0x2d, 0x3d, 0x3f, 0xfc, 0x98, 0xbe, 0x4b, 0xd1, 0x55, 0xcf, 0x98, + 0x19, 0xdc, 0x92, 0xcb, 0xfc, 0x2d, 0x21, 0xef, 0xcc, 0xee, 0x84, 0x27, 0x08, 0xf5, 0x7a, 0x03, + 0x8a, 0x6d, 0x61, 0x8b, 0x20, 0x59, 0xcc, 0x0b, 0xbb, 0x03, 0x33, 0x2c, 0xd5, 0x7e, 0x47, 0x11, + 0x4a, 0x87, 0xc7, 0x77, 0x96, 0x94, 0x19, 0xac, 0x3f, 0x3f, 0x33, 0x58, 0xff, 0xe7, 0xcc, 0x0c, + 0xa6, 0x79, 0x70, 0xf3, 0xd4, 0x1d, 0x4b, 0x3e, 0x0e, 0x2a, 0x26, 0x51, 0x72, 0xa4, 0x41, 0x62, + 0xeb, 0x6b, 0x23, 0x8c, 0xfd, 0x5d, 0xe5, 0x99, 0xea, 0xcc, 0xf5, 0x5e, 0x9c, 0x5a, 0xfb, 0x73, + 0x1e, 0xf3, 0xbd, 0xd6, 0x6f, 0x27, 0x14, 0xcd, 0xef, 0xd5, 0xc9, 0xc2, 0x88, 0x2d, 0xb6, 0x67, + 0xa5, 0x24, 0x96, 0xe9, 0x6f, 0xcd, 0xf6, 0xb5, 0x90, 0x56, 0xde, 0x9f, 0xe4, 0xe1, 0xd2, 0x3c, + 0xf2, 0xcc, 0x34, 0xd9, 0xca, 0x93, 0xa5, 0xc9, 0xbe, 0x09, 0x45, 0x06, 0x0b, 0x3d, 0x08, 0x70, + 0x6c, 0x39, 0x29, 0x1d, 0x5b, 0x51, 0x4c, 0x9e, 0x85, 0x45, 0xbd, 0x62, 0x45, 0x99, 0xdb, 0xd0, + 0xd4, 0xd7, 0xe9, 0x05, 0x68, 0x44, 0xca, 0x8b, 0xc8, 0xe7, 0xd3, 0xc9, 0x0a, 0x79, 0xca, 0xb6, + 0x8b, 0x52, 0x87, 0xa4, 0xd2, 0x31, 0x60, 0x7d, 0xa3, 0xf4, 0x01, 0x3c, 0x22, 0xb7, 0x99, 0x4e, + 0x7c, 0xa8, 0xc1, 0x62, 0x7b, 0xec, 0x06, 0xee, 0x44, 0x36, 0xc3, 0x1d, 0x21, 0xc4, 0xe4, 0x25, + 0xdc, 0x48, 0xd6, 0x79, 0xc4, 0x62, 0x22, 0x2c, 0xca, 0x71, 0x6a, 0xd0, 0xaa, 0x96, 0x82, 0x4d, + 0x09, 0x85, 0x12, 0xd4, 0x9d, 0xe9, 0xb0, 0x77, 0xdc, 0x35, 0xeb, 0x5c, 0x72, 0x62, 0x04, 0x03, + 0x84, 0xd2, 0x06, 0x06, 0xa6, 0x84, 0xa2, 0x7d, 0x53, 0x81, 0x73, 0x59, 0xed, 0x20, 0x97, 0xa0, + 0x30, 0xcc, 0xcc, 0xcb, 0x38, 0x64, 0xae, 0xdc, 0x25, 0xfa, 0xd7, 0x3e, 0xf4, 0xc7, 0x27, 0xce, + 0x44, 0x36, 0x56, 0x96, 0xc0, 0x26, 0xd0, 0x1f, 0x7b, 0xf8, 0x3f, 0xb9, 0x22, 0x8e, 0x9c, 0x7c, + 0x2a, 0x93, 0x23, 0xfe, 0xd1, 0x74, 0x80, 0x5a, 0xbf, 0xdd, 0x1a, 0xb1, 0x74, 0x00, 0x2f, 0x41, + 0x81, 0x56, 0x2b, 0x31, 0x7b, 0xe9, 0xfc, 0xd1, 0x1b, 0x75, 0x8e, 0xc4, 0x6a, 0x15, 0x38, 0x27, + 0x03, 0x13, 0x91, 0xb5, 0x7b, 0xb0, 0x16, 0xc7, 0x20, 0x46, 0x3c, 0x22, 0x6c, 0xe9, 0xb6, 0xca, + 0x39, 0xed, 0xf8, 0x3e, 0x73, 0x98, 0xd9, 0x79, 0xea, 0xa7, 0xef, 0x5c, 0x01, 0xfa, 0x93, 0xd1, + 0x64, 0x45, 0x8c, 0xd5, 0xbe, 0x95, 0x83, 0x73, 0x91, 0x8f, 0xbe, 0x58, 0x43, 0xbf, 0xb4, 0x0e, + 0xa3, 0x7a, 0xcc, 0xa1, 0x51, 0xc8, 0x8d, 0xe9, 0x06, 0xce, 0xf1, 0xa3, 0xda, 0x87, 0xad, 0x59, + 0xf8, 0xe4, 0x79, 0x58, 0xc6, 0xb0, 0x4e, 0x23, 0xa7, 0xe7, 0xca, 0xdb, 0xec, 0x50, 0x00, 0xcd, + 0xa8, 0x5c, 0xfb, 0xb1, 0x02, 0xdb, 0xdc, 0xcd, 0xa3, 0xe1, 0x78, 0x43, 0x7c, 0x25, 0xe8, 0xb9, + 0xef, 0x8f, 0xc3, 0xf3, 0x7e, 0x6c, 0x1f, 0x7b, 0x2e, 0xee, 0xcd, 0x93, 0xfa, 0xda, 0xec, 0xd6, + 0x92, 0x9b, 0x18, 0xaa, 0x8c, 0xbf, 0xa2, 0x17, 0x58, 0x80, 0x89, 0x21, 0x05, 0xc8, 0x01, 0x26, + 0x10, 0x43, 0xfb, 0x0d, 0xb8, 0x3c, 0xff, 0x03, 0xe4, 0x73, 0xb0, 0x8a, 0xb9, 0xb7, 0xba, 0xa3, + 0xa3, 0xb1, 0xd3, 0x77, 0x85, 0x66, 0x4f, 0x68, 0x63, 0xe5, 0x32, 0x16, 0x79, 0x8d, 0x07, 0x3c, + 0x38, 0xc2, 0xac, 0x5e, 0x9c, 0x28, 0xe6, 0x4b, 0x25, 0x73, 0xd3, 0xbe, 0xa6, 0x00, 0x49, 0xf3, + 0x20, 0x1f, 0x81, 0x95, 0x6e, 0xa7, 0x62, 0x4d, 0x9c, 0xf1, 0xa4, 0xea, 0x4f, 0xc7, 0x3c, 0xec, + 0x19, 0xf3, 0x7f, 0x9f, 0xf4, 0x6c, 0xf6, 0x1e, 0x74, 0xec, 0x4f, 0xc7, 0x66, 0x0c, 0x0f, 0x73, + 0x3c, 0xb9, 0xee, 0x97, 0xfa, 0xce, 0xa3, 0x78, 0x8e, 0x27, 0x0e, 0x8b, 0xe5, 0x78, 0xe2, 0x30, + 0xed, 0x7b, 0x0a, 0x5c, 0x14, 0xc6, 0x91, 0xfd, 0x8c, 0xba, 0x54, 0x30, 0xca, 0xcb, 0x58, 0xc4, + 0xd9, 0x9d, 0x27, 0xa1, 0x6f, 0x88, 0x40, 0x48, 0x58, 0x41, 0x14, 0xd5, 0x19, 0x2d, 0xf9, 0x24, + 0x14, 0xac, 0x89, 0x3f, 0x3a, 0x45, 0x24, 0x24, 0x35, 0x1c, 0xd1, 0x89, 0x3f, 0x42, 0x16, 0x48, + 0xa9, 0xb9, 0x70, 0x4e, 0xae, 0x9c, 0xa8, 0x31, 0x69, 0xc0, 0x12, 0x0f, 0x79, 0x97, 0xb0, 0x3b, + 0x98, 0xd3, 0xa6, 0x9d, 0x75, 0x11, 0x6e, 0x89, 0xc7, 0x79, 0x35, 0x05, 0x0f, 0xed, 0xf7, 0x14, + 0x28, 0x51, 0xc1, 0x06, 0x2f, 0xa5, 0xef, 0x75, 0x4a, 0xc7, 0xe5, 0x60, 0x61, 0x46, 0x13, 0xb2, + 0x3f, 0xd5, 0x69, 0xfc, 0x32, 0xac, 0x27, 0x08, 0x88, 0x86, 0x81, 0x36, 0x06, 0x5e, 0xcf, 0x61, + 0x29, 0x63, 0x98, 0x09, 0x4a, 0x0c, 0xa6, 0xfd, 0x96, 0x02, 0xe7, 0x5a, 0x5f, 0x9a, 0x38, 0xec, + 0xd9, 0xd6, 0x9c, 0x0e, 0xc4, 0x7a, 0xa7, 0xc2, 0x9a, 0xb0, 0xb2, 0x65, 0x41, 0x00, 0x98, 0xb0, + 0xc6, 0x61, 0x66, 0x58, 0x4a, 0xaa, 0x50, 0xe4, 0xe7, 0x4b, 0xc0, 0xc3, 0xb3, 0x5e, 0x96, 0x74, + 0x23, 0x11, 0x63, 0x8e, 0x44, 0x5b, 0x82, 0x5b, 0x18, 0xa7, 0x31, 0x43, 0x6a, 0xed, 0x3f, 0x14, + 0xd8, 0x9c, 0x41, 0x43, 0x5e, 0x87, 0x05, 0x74, 0x50, 0xe4, 0xa3, 0x77, 0x69, 0xc6, 0x27, 0x26, + 0xbd, 0xe3, 0x83, 0x5b, 0xec, 0x20, 0x3a, 0xa1, 0x3f, 0x4c, 0x46, 0x45, 0xde, 0x84, 0x65, 0xbd, + 0xdf, 0xe7, 0xb7, 0xb3, 0x5c, 0xec, 0x76, 0x36, 0xe3, 0x8b, 0x2f, 0x86, 0xf8, 0xec, 0x76, 0xc6, + 0x5c, 0x65, 0xfa, 0x7d, 0x9b, 0x3b, 0x5f, 0x46, 0xfc, 0xb6, 0x3f, 0x0e, 0x6b, 0x71, 0xe4, 0x27, + 0xf2, 0x17, 0x7b, 0x5b, 0x01, 0x35, 0x5e, 0x87, 0x5f, 0x4c, 0xa0, 0xa8, 0xac, 0x61, 0x7e, 0xcc, + 0xa4, 0xfa, 0x83, 0x1c, 0x9c, 0xcf, 0xec, 0x61, 0xf2, 0x02, 0x2c, 0xea, 0xa3, 0x51, 0x6d, 0x97, + 0xcf, 0x2a, 0x2e, 0x21, 0xa1, 0xd2, 0x3b, 0x76, 0x79, 0x65, 0x48, 0xe4, 0x25, 0x28, 0x32, 0xeb, + 0x80, 0x5d, 0xb1, 0xe1, 0x60, 0xe4, 0x1b, 0x6e, 0xba, 0x10, 0x0f, 0x94, 0x2a, 0x10, 0xc9, 0x1e, + 0xac, 0xf1, 0x98, 0x31, 0xa6, 0x7b, 0xe4, 0x7e, 0x25, 0x8c, 0xd8, 0x8f, 0x49, 0x05, 0x84, 0x26, + 0xdd, 0x1e, 0xb3, 0x32, 0x39, 0x6a, 0x4a, 0x9c, 0x8a, 0xd4, 0x41, 0x45, 0x9e, 0x32, 0x27, 0x16, + 0xad, 0x15, 0xa3, 0xf8, 0xb0, 0x4a, 0xcc, 0xe0, 0x95, 0xa2, 0x0c, 0x87, 0x4b, 0x0f, 0x02, 0xef, + 0x68, 0x78, 0xe2, 0x0e, 0x27, 0xbf, 0xb8, 0xe1, 0x8a, 0xbe, 0x71, 0xaa, 0xe1, 0xfa, 0xa3, 0x02, + 0x5b, 0xcc, 0x49, 0x32, 0x2a, 0xd1, 0x48, 0x01, 0xba, 0x51, 0xa2, 0xa1, 0xf7, 0x33, 0x1e, 0x15, + 0x65, 0x17, 0x96, 0x58, 0xb4, 0x1a, 0xb1, 0x32, 0x9e, 0xce, 0xac, 0x02, 0xc3, 0x39, 0xb8, 0xc5, + 0xc4, 0x17, 0xe6, 0x29, 0x19, 0x98, 0x82, 0x94, 0x1c, 0x40, 0xa9, 0x32, 0x70, 0x9d, 0xe1, 0x74, + 0xd4, 0x39, 0xdd, 0x0b, 0xea, 0x16, 0x6f, 0xcb, 0x4a, 0x8f, 0x91, 0xe1, 0xcb, 0x2b, 0xee, 0xe4, + 0x32, 0x23, 0xd2, 0x09, 0x9d, 0xa7, 0x0a, 0xa8, 0x78, 0xfd, 0xf0, 0x9c, 0xfe, 0x49, 0x02, 0x91, + 0x2e, 0xee, 0x19, 0xc8, 0xbd, 0xab, 0x6c, 0x58, 0xab, 0x3b, 0xc1, 0xa4, 0x33, 0x76, 0x86, 0x01, + 0x46, 0xb9, 0x3c, 0x45, 0x14, 0xb0, 0x8b, 0x22, 0x83, 0x33, 0xaa, 0x4c, 0x27, 0x21, 0x29, 0x53, + 0xc8, 0xc6, 0xd9, 0x51, 0x79, 0x69, 0xcf, 0x1b, 0x3a, 0x03, 0xef, 0xab, 0xc2, 0xc7, 0x94, 0xc9, + 0x4b, 0x87, 0x02, 0x68, 0x46, 0xe5, 0xda, 0x67, 0x53, 0xe3, 0xc6, 0x6a, 0x59, 0x82, 0x25, 0x1e, + 0x81, 0x80, 0x79, 0xe4, 0xb7, 0x8d, 0xe6, 0x6e, 0xad, 0xb9, 0xaf, 0x2a, 0x64, 0x0d, 0xa0, 0x6d, + 0xb6, 0x2a, 0x86, 0x65, 0xd1, 0xdf, 0x39, 0xfa, 0x9b, 0xbb, 0xeb, 0xef, 0x75, 0xeb, 0x6a, 0x5e, + 0xf2, 0xd8, 0x2f, 0x68, 0x3f, 0x52, 0xe0, 0x42, 0xf6, 0x50, 0x92, 0x0e, 0x60, 0xcc, 0x06, 0xfe, + 0x96, 0xfe, 0x91, 0xb9, 0xe3, 0x9e, 0x09, 0x4e, 0xc6, 0x7e, 0x98, 0xb0, 0x98, 0x02, 0x39, 0xf1, + 0xf6, 0xc5, 0x9c, 0x14, 0xbd, 0xbe, 0x99, 0xf3, 0xfa, 0x5a, 0x05, 0xb6, 0x66, 0xf1, 0x88, 0x37, + 0x75, 0x1d, 0x4a, 0x7a, 0xbb, 0x5d, 0xaf, 0x55, 0xf4, 0x4e, 0xad, 0xd5, 0x54, 0x15, 0xb2, 0x0c, + 0x0b, 0xfb, 0x66, 0xab, 0xdb, 0x56, 0x73, 0xda, 0xb7, 0x15, 0x58, 0xad, 0x45, 0x56, 0x67, 0xef, + 0x75, 0xf1, 0x7d, 0x34, 0xb6, 0xf8, 0xb6, 0xc2, 0xe8, 0x26, 0xe1, 0x07, 0x4e, 0xb5, 0xf2, 0xde, + 0xcd, 0xc1, 0x46, 0x8a, 0x86, 0x58, 0xb0, 0xa4, 0xdf, 0xb3, 0x5a, 0xb5, 0xdd, 0x0a, 0xaf, 0xd9, + 0x95, 0xc8, 0x5c, 0x0a, 0xf3, 0x5d, 0xa5, 0xbe, 0xc2, 0x3c, 0x82, 0x1f, 0x06, 0xb6, 0xef, 0xf5, + 0xa5, 0xe4, 0xb7, 0xd5, 0x33, 0xa6, 0xe0, 0x84, 0x27, 0xd9, 0x57, 0xa7, 0x63, 0x17, 0xd9, 0xe6, + 0x62, 0x7a, 0xdd, 0x10, 0x9e, 0x66, 0x8c, 0xfe, 0x1b, 0x0e, 0x2d, 0x4f, 0xb3, 0x8e, 0xf8, 0x91, + 0x26, 0x2c, 0xee, 0x7b, 0x93, 0xea, 0xf4, 0x01, 0x5f, 0xbf, 0x97, 0xa3, 0xec, 0x47, 0xd5, 0xe9, + 0x83, 0x34, 0x5b, 0x54, 0x59, 0xb2, 0xe8, 0x3d, 0x31, 0x96, 0x9c, 0x4b, 0xd2, 0x89, 0xb1, 0xf0, + 0x44, 0x4e, 0x8c, 0x3b, 0xab, 0x50, 0xe2, 0x77, 0x28, 0xbc, 0x9e, 0xfc, 0x40, 0x81, 0xad, 0x59, + 0x3d, 0x47, 0xaf, 0x65, 0xf1, 0x60, 0x05, 0x17, 0xc2, 0xf4, 0x18, 0xf1, 0x28, 0x05, 0x02, 0x8d, + 0xbc, 0x01, 0xa5, 0x5a, 0x10, 0x4c, 0xdd, 0xb1, 0xf5, 0x52, 0xd7, 0xac, 0xf1, 0xe9, 0xfa, 0xf4, + 0xbf, 0xbe, 0x73, 0x65, 0x13, 0x7d, 0x3e, 0xc6, 0x76, 0xf0, 0x92, 0x3d, 0x1d, 0x7b, 0xb1, 0x54, + 0x02, 0x32, 0x05, 0x95, 0xa2, 0x9d, 0x69, 0xdf, 0x73, 0xc5, 0x1d, 0x42, 0x38, 0x74, 0x73, 0x98, + 0x7c, 0xa6, 0x09, 0x98, 0xf6, 0x0d, 0x05, 0xb6, 0x67, 0x0f, 0x13, 0x3d, 0x27, 0x3b, 0xcc, 0xa4, + 0x4a, 0xb8, 0x54, 0xe3, 0x39, 0x19, 0xda, 0x5d, 0xc9, 0x3c, 0x05, 0x22, 0x25, 0x0a, 0x53, 0xe3, + 0xe7, 0x52, 0xf9, 0xb0, 0xe3, 0x44, 0x02, 0x51, 0xbb, 0x0f, 0x9b, 0x33, 0x06, 0x95, 0x7c, 0x22, + 0x33, 0xe9, 0x0e, 0xba, 0x29, 0xc9, 0x49, 0x77, 0x62, 0xd9, 0xdb, 0x24, 0xb8, 0xf6, 0x6f, 0x39, + 0xb8, 0x40, 0x57, 0xd7, 0xc0, 0x0d, 0x02, 0x3d, 0xca, 0x4f, 0x4b, 0x77, 0xc5, 0x57, 0x60, 0xf1, + 0xf8, 0xc9, 0x54, 0xc5, 0x0c, 0x9d, 0x10, 0xc0, 0x13, 0x4b, 0x38, 0xc7, 0xd0, 0xff, 0xc9, 0x55, + 0x90, 0x93, 0x9b, 0xe7, 0x31, 0xbc, 0x69, 0x6e, 0x4b, 0x31, 0x97, 0x47, 0x61, 0x1e, 0xe2, 0x57, + 0x61, 0x01, 0xf5, 0x29, 0xfc, 0xec, 0x10, 0x32, 0x7f, 0x76, 0xed, 0x50, 0xdb, 0x62, 0x32, 0x02, + 0xf2, 0x21, 0x80, 0x28, 0x33, 0x04, 0x3f, 0x1c, 0x84, 0x9e, 0x21, 0x4c, 0x0e, 0x61, 0x2e, 0x9f, + 0x1c, 0x3a, 0x3c, 0xdd, 0x42, 0x19, 0x36, 0x44, 0x8f, 0x8f, 0x44, 0x54, 0x44, 0xfe, 0x8a, 0xb9, + 0xce, 0x0a, 0x6a, 0x23, 0x11, 0x19, 0xf1, 0x5a, 0x2a, 0x41, 0x33, 0x06, 0x47, 0x4e, 0x64, 0x61, + 0xbe, 0x96, 0xca, 0xc2, 0x5c, 0x64, 0x58, 0x72, 0xaa, 0x65, 0xed, 0x9f, 0x72, 0xb0, 0x7c, 0x8f, + 0x4a, 0x65, 0xa8, 0x6b, 0x98, 0xaf, 0xbb, 0xb8, 0x0d, 0xa5, 0xba, 0xef, 0xf0, 0xe7, 0x22, 0xee, + 0x53, 0xc2, 0x7c, 0xba, 0x07, 0xbe, 0x23, 0x5e, 0x9e, 0x02, 0x53, 0x46, 0x7a, 0x8c, 0x3f, 0xfa, + 0x1d, 0x58, 0x64, 0xcf, 0x77, 0x5c, 0x8d, 0x26, 0xe4, 0xf2, 0xb0, 0x46, 0x2f, 0xb2, 0x62, 0xe9, + 0x85, 0x83, 0x3d, 0x01, 0xca, 0x42, 0x22, 0x8f, 0xf1, 0x2a, 0x69, 0x56, 0x16, 0x4e, 0xa7, 0x59, + 0x91, 0x62, 0xd9, 0x2d, 0x9e, 0x26, 0x96, 0xdd, 0xf6, 0x6b, 0x50, 0x92, 0xea, 0xf3, 0x44, 0x62, + 0xfa, 0xd7, 0x73, 0xb0, 0x8a, 0xad, 0x0a, 0x6d, 0x79, 0x7e, 0x39, 0xf5, 0x44, 0x1f, 0x8d, 0xe9, + 0x89, 0xb6, 0xe4, 0xf1, 0x62, 0x2d, 0x9b, 0xa3, 0x20, 0xba, 0x03, 0x1b, 0x29, 0x44, 0xf2, 0x32, + 0x2c, 0xd0, 0xea, 0x8b, 0x7b, 0xb5, 0x9a, 0x9c, 0x01, 0x51, 0xdc, 0x63, 0xda, 0xf0, 0xc0, 0x64, + 0xd8, 0xda, 0x7f, 0x2a, 0xb0, 0xc2, 0xd3, 0x8e, 0x0c, 0x0f, 0xfd, 0xc7, 0x76, 0xe7, 0xf5, 0x64, + 0x77, 0xb2, 0xe8, 0x2a, 0xbc, 0x3b, 0xff, 0xb7, 0x3b, 0xf1, 0xb5, 0x58, 0x27, 0x6e, 0x86, 0x51, + 0x10, 0x45, 0x73, 0xe6, 0xf4, 0xe1, 0xf7, 0x31, 0x2e, 0x70, 0x1c, 0x91, 0x7c, 0x1e, 0x96, 0x9b, + 0xee, 0xc3, 0xd8, 0xf5, 0xf4, 0xfa, 0x0c, 0xa6, 0x2f, 0x86, 0x88, 0x6c, 0x4d, 0xe1, 0xc9, 0x3e, + 0x74, 0x1f, 0xda, 0xa9, 0x97, 0xc3, 0x88, 0x25, 0xbd, 0xa1, 0xc6, 0xc9, 0x9e, 0x64, 0xea, 0x73, + 0x07, 0x57, 0x0c, 0x18, 0xf4, 0xcd, 0x3c, 0x40, 0xe4, 0x1b, 0x48, 0x17, 0x60, 0xcc, 0x68, 0x42, + 0x68, 0xf6, 0x11, 0x24, 0xcf, 0x71, 0x61, 0x4b, 0x71, 0x9d, 0x6b, 0xa0, 0x73, 0xb3, 0xa3, 0x54, + 0xa2, 0x2e, 0xba, 0xc2, 0x9d, 0xd1, 0xfa, 0xee, 0xc0, 0x61, 0x7b, 0x7b, 0x7e, 0xe7, 0x1a, 0x06, + 0x25, 0x0e, 0xa1, 0x33, 0xd2, 0x4d, 0xa3, 0xcb, 0xda, 0x2e, 0x45, 0x48, 0xf9, 0xdb, 0x16, 0x9e, + 0xcc, 0xdf, 0xb6, 0x0d, 0xcb, 0xde, 0xf0, 0x2d, 0x77, 0x38, 0xf1, 0xc7, 0x8f, 0x50, 0xed, 0x1e, + 0xe9, 0xf3, 0x68, 0x17, 0xd4, 0x44, 0x19, 0x1b, 0x07, 0x3c, 0x73, 0x43, 0x7c, 0x79, 0x18, 0x42, + 0x60, 0xe8, 0x2f, 0xbc, 0xa0, 0x2e, 0xde, 0x29, 0x14, 0x17, 0xd5, 0xa5, 0x3b, 0x85, 0x62, 0x51, + 0x5d, 0xbe, 0x53, 0x28, 0x2e, 0xab, 0x60, 0x4a, 0x6f, 0x66, 0xe1, 0x9b, 0x98, 0xf4, 0x8c, 0x15, + 0x7f, 0xa2, 0xd2, 0x7e, 0x96, 0x03, 0x92, 0xae, 0x06, 0xf9, 0x28, 0x94, 0xd8, 0x06, 0x6b, 0x8f, + 0x83, 0x2f, 0x73, 0x77, 0x03, 0x16, 0x76, 0x49, 0x02, 0xcb, 0x61, 0x97, 0x18, 0xd8, 0x0c, 0xbe, + 0x3c, 0x20, 0x9f, 0x83, 0xb3, 0xd8, 0xbd, 0x23, 0x77, 0xec, 0xf9, 0x7d, 0x1b, 0x63, 0xe4, 0x3a, + 0x03, 0x9e, 0x1a, 0xf2, 0x05, 0xcc, 0x61, 0x9c, 0x2e, 0x9e, 0x31, 0x0c, 0xe8, 0x02, 0xd8, 0x46, + 0xcc, 0x36, 0x43, 0x24, 0x1d, 0x50, 0x65, 0xfa, 0xc3, 0xe9, 0x60, 0xc0, 0x47, 0xb6, 0x4c, 0x6f, + 0xf4, 0xc9, 0xb2, 0x19, 0x8c, 0xd7, 0x22, 0xc6, 0x7b, 0xd3, 0xc1, 0x80, 0xbc, 0x02, 0xe0, 0x0f, + 0xed, 0x13, 0x2f, 0x08, 0xd8, 0x63, 0x4e, 0xe8, 0xad, 0x1c, 0x41, 0xe5, 0xc1, 0xf0, 0x87, 0x0d, + 0x06, 0x24, 0xbf, 0x06, 0x18, 0xad, 0x01, 0xc3, 0x98, 0x30, 0x6b, 0x24, 0x9e, 0xbd, 0x45, 0x00, + 0xe3, 0xce, 0xd1, 0x47, 0xae, 0xe5, 0x7d, 0x55, 0xb8, 0x7a, 0x7c, 0x06, 0x36, 0xb8, 0xf1, 0xf0, + 0x3d, 0x6f, 0x72, 0xcc, 0xaf, 0x12, 0xef, 0xe5, 0x1e, 0x22, 0xdd, 0x25, 0xfe, 0xae, 0x00, 0xa0, + 0xdf, 0xb3, 0x44, 0x84, 0xb0, 0x9b, 0xb0, 0x40, 0x2f, 0x48, 0x42, 0xd1, 0x82, 0x6a, 0x6a, 0xe4, + 0x2b, 0xab, 0xa9, 0x11, 0x83, 0xae, 0x46, 0x13, 0x8d, 0xea, 0x85, 0x92, 0x05, 0x57, 0x23, 0xb3, + 0xb3, 0x8f, 0x45, 0x68, 0xe6, 0x58, 0xa4, 0x0e, 0x10, 0xc5, 0xec, 0xe2, 0x22, 0xff, 0x46, 0x14, + 0xfc, 0x86, 0x17, 0xf0, 0x2c, 0x11, 0x51, 0xdc, 0x2f, 0x79, 0xfa, 0x44, 0x68, 0xe4, 0x2e, 0x14, + 0x3a, 0x4e, 0xe8, 0x8b, 0x3b, 0x23, 0x92, 0xd9, 0x33, 0x3c, 0x75, 0x67, 0x14, 0xcd, 0x6c, 0x6d, + 0xe2, 0xc4, 0x32, 0x1c, 0x23, 0x13, 0x62, 0xc0, 0x22, 0x4f, 0xcb, 0x3e, 0x23, 0x02, 0x26, 0xcf, + 0xca, 0xce, 0xe3, 0x5e, 0x23, 0x50, 0x96, 0x29, 0x78, 0x02, 0xf6, 0xdb, 0x90, 0xb7, 0xac, 0x06, + 0x8f, 0xdf, 0xb1, 0x1a, 0x5d, 0xbf, 0x2c, 0xab, 0xc1, 0xde, 0x7d, 0x83, 0xe0, 0x44, 0x22, 0xa3, + 0xc8, 0xe4, 0x63, 0x50, 0x92, 0x84, 0x62, 0x1e, 0xf9, 0x06, 0xfb, 0x40, 0xf2, 0x76, 0x92, 0x37, + 0x0d, 0x09, 0x9b, 0xd4, 0x41, 0xbd, 0x3b, 0x7d, 0xe0, 0xea, 0xa3, 0x11, 0xba, 0x41, 0xbe, 0xe5, + 0x8e, 0x99, 0xd8, 0x56, 0x8c, 0x42, 0x46, 0xa3, 0x8f, 0x44, 0x5f, 0x94, 0xca, 0xca, 0xa6, 0x24, + 0x25, 0x69, 0xc3, 0x86, 0xe5, 0x4e, 0xa6, 0x23, 0x66, 0x5f, 0xb3, 0xe7, 0x8f, 0xe9, 0xfd, 0x86, + 0xc5, 0xc9, 0xc1, 0xe8, 0xba, 0x01, 0x2d, 0x14, 0x46, 0x4d, 0x87, 0xfe, 0x38, 0x71, 0xd7, 0x49, + 0x13, 0x6b, 0xae, 0x3c, 0xe4, 0xf4, 0x54, 0x8d, 0xdf, 0x9a, 0xf0, 0x54, 0x15, 0xb7, 0xa6, 0xe8, + 0xae, 0xf4, 0xa1, 0x8c, 0x58, 0x6e, 0xf8, 0x32, 0x28, 0xc5, 0x72, 0x8b, 0x45, 0x70, 0xfb, 0x5e, + 0x41, 0x0a, 0x27, 0xca, 0xc7, 0xe2, 0x75, 0x80, 0x3b, 0xbe, 0x37, 0x6c, 0xb8, 0x93, 0x63, 0xbf, + 0x2f, 0x85, 0x94, 0x2b, 0x7d, 0xd1, 0xf7, 0x86, 0xf6, 0x09, 0x82, 0x7f, 0xf6, 0xce, 0x15, 0x09, + 0xc9, 0x94, 0xfe, 0x27, 0x1f, 0x84, 0x65, 0xfa, 0xab, 0x13, 0x59, 0x09, 0x31, 0x9d, 0x2c, 0x52, + 0xb3, 0xa4, 0x1b, 0x11, 0x02, 0x79, 0x0d, 0xd3, 0xcc, 0x78, 0xa3, 0x89, 0x24, 0xbc, 0x8a, 0x9c, + 0x32, 0xde, 0x68, 0x92, 0x8c, 0x10, 0x2d, 0x21, 0x93, 0x6a, 0x58, 0x75, 0x91, 0x19, 0x8a, 0x67, + 0xb3, 0x41, 0xc5, 0x23, 0x9f, 0x6b, 0xb6, 0x08, 0x4d, 0x2b, 0xa7, 0xfc, 0x4d, 0x90, 0x61, 0x25, + 0xac, 0xea, 0x2e, 0x7b, 0x29, 0xe2, 0x42, 0x2d, 0xab, 0x44, 0x70, 0xdc, 0xb7, 0x7b, 0x08, 0x8e, + 0x55, 0x22, 0x44, 0x26, 0x3b, 0xb0, 0xce, 0x64, 0xfc, 0x30, 0xc3, 0x24, 0x17, 0x71, 0x71, 0x6f, + 0x8b, 0x52, 0x50, 0xca, 0x9f, 0x4f, 0x10, 0x90, 0x3d, 0x58, 0xc0, 0xbb, 0x26, 0x77, 0x0d, 0xb8, + 0x28, 0xab, 0x09, 0x92, 0xeb, 0x08, 0xf7, 0x15, 0x54, 0x10, 0xc8, 0xfb, 0x0a, 0xa2, 0x92, 0x4f, + 0x03, 0x18, 0xc3, 0xb1, 0x3f, 0x18, 0x60, 0xf0, 0xe4, 0x22, 0x5e, 0xa5, 0x9e, 0x8e, 0xaf, 0x47, + 0xe4, 0x12, 0x21, 0xf1, 0x40, 0x7f, 0xf8, 0xdb, 0x4e, 0x84, 0x58, 0x96, 0x78, 0x69, 0x35, 0x58, + 0x64, 0x8b, 0x11, 0x03, 0x91, 0xf3, 0xd4, 0x2a, 0x52, 0x18, 0x6b, 0x16, 0x88, 0x9c, 0xc3, 0xd3, + 0x81, 0xc8, 0x25, 0x02, 0xed, 0x2e, 0x9c, 0xcb, 0x6a, 0x58, 0xec, 0x76, 0xac, 0x9c, 0xf6, 0x76, + 0xfc, 0xdd, 0x3c, 0xac, 0x20, 0x37, 0xb1, 0x0b, 0xeb, 0xb0, 0x6a, 0x4d, 0x1f, 0x84, 0x51, 0xba, + 0xc4, 0x6e, 0x8c, 0xf5, 0x0b, 0xe4, 0x02, 0xf9, 0x0d, 0x2f, 0x46, 0x41, 0x0c, 0x58, 0x13, 0x27, + 0xc1, 0xbe, 0xf0, 0x1c, 0x08, 0x63, 0x80, 0x8b, 0x48, 0x93, 0xe9, 0x0c, 0xbb, 0x09, 0xa2, 0xe8, + 0x3c, 0xc8, 0x3f, 0xc9, 0x79, 0x50, 0x38, 0xd5, 0x79, 0xf0, 0x26, 0xac, 0x88, 0xaf, 0xe1, 0x4e, + 0xbe, 0xf0, 0xde, 0x76, 0xf2, 0x18, 0x33, 0x52, 0x0f, 0x77, 0xf4, 0xc5, 0xb9, 0x3b, 0x3a, 0x3e, + 0x8c, 0x8a, 0x55, 0x36, 0x42, 0x58, 0x7a, 0x63, 0xc7, 0x14, 0x94, 0xfb, 0x95, 0xf6, 0xcf, 0x71, + 0x4a, 0xbe, 0x0c, 0xcb, 0x75, 0x5f, 0xbc, 0x89, 0x49, 0x8f, 0x11, 0x03, 0x01, 0x94, 0xc5, 0x85, + 0x10, 0x33, 0x3c, 0xdd, 0xf2, 0xef, 0xc7, 0xe9, 0xf6, 0x1a, 0x00, 0x77, 0x49, 0x89, 0x52, 0xc7, + 0xe1, 0x92, 0x11, 0x11, 0x4a, 0xe2, 0x6f, 0x22, 0x12, 0x32, 0xdd, 0x9d, 0xb8, 0xb9, 0x8d, 0xde, + 0xeb, 0xf9, 0xd3, 0xe1, 0x24, 0x96, 0x6b, 0x59, 0x78, 0xb0, 0x3a, 0xbc, 0x4c, 0xde, 0x1e, 0x12, + 0x64, 0xef, 0xef, 0x80, 0x90, 0x4f, 0x85, 0xc6, 0x8f, 0x4b, 0xf3, 0x7a, 0x48, 0x4b, 0xf5, 0xd0, + 0x4c, 0x93, 0x47, 0xed, 0x47, 0x8a, 0x9c, 0x80, 0xe1, 0xe7, 0x18, 0xea, 0x57, 0x01, 0x42, 0xa3, + 0x04, 0x31, 0xd6, 0xec, 0xbe, 0x14, 0x42, 0xe5, 0x5e, 0x8e, 0x70, 0xa5, 0xd6, 0xe4, 0xdf, 0xaf, + 0xd6, 0x74, 0xa0, 0xd4, 0xfa, 0xd2, 0xc4, 0x89, 0xac, 0x58, 0xc0, 0x0a, 0x25, 0x59, 0xdc, 0x99, + 0xf2, 0x3b, 0xcf, 0xe1, 0xd9, 0x10, 0xc9, 0xc1, 0x33, 0x44, 0x60, 0x89, 0x50, 0xfb, 0x6f, 0x05, + 0xd6, 0x65, 0xb7, 0xfb, 0x47, 0xc3, 0x1e, 0xf9, 0x04, 0x8b, 0x07, 0xab, 0xc4, 0xae, 0x2c, 0x12, + 0x12, 0xdd, 0x72, 0x1f, 0x0d, 0x7b, 0x4c, 0x00, 0x72, 0x1e, 0xca, 0x95, 0xa5, 0x84, 0xe4, 0x01, + 0xac, 0xb4, 0xfd, 0xc1, 0x80, 0x8a, 0x35, 0xe3, 0xb7, 0xf8, 0x05, 0x80, 0x32, 0x4a, 0x3e, 0x8d, + 0x88, 0x0a, 0xed, 0x3c, 0xcb, 0xef, 0xb9, 0x9b, 0x23, 0xba, 0xdf, 0x7b, 0x9c, 0x2e, 0x62, 0xfb, + 0x36, 0xfa, 0xc9, 0xc9, 0x3c, 0xa3, 0xb3, 0x29, 0x9e, 0x48, 0x40, 0xae, 0x25, 0x2d, 0xc6, 0x7a, + 0xce, 0x39, 0x9b, 0xb4, 0x9f, 0x28, 0x40, 0xd2, 0x4d, 0x93, 0xb7, 0x3e, 0xe5, 0xff, 0x40, 0x14, + 0x4e, 0x88, 0x90, 0x85, 0x27, 0x11, 0x21, 0xb5, 0xef, 0x28, 0x70, 0x2e, 0xab, 0x1f, 0xe8, 0x09, + 0x22, 0x1f, 0x29, 0xe1, 0x81, 0x86, 0x27, 0x88, 0x7c, 0x0a, 0xc5, 0x8f, 0xb5, 0x04, 0x51, 0xb2, + 0x72, 0xb9, 0x27, 0xa9, 0x5c, 0xf9, 0xf7, 0x15, 0x58, 0xaf, 0xe9, 0x0d, 0x9e, 0x77, 0x82, 0x3d, + 0x53, 0x5d, 0x85, 0xa7, 0x6b, 0x7a, 0xc3, 0x6e, 0xb7, 0xea, 0xb5, 0xca, 0x7d, 0x3b, 0x33, 0x9c, + 0xf4, 0xd3, 0xf0, 0x54, 0x1a, 0x25, 0x7a, 0xce, 0xba, 0x04, 0x5b, 0xe9, 0x62, 0x11, 0x72, 0x3a, + 0x9b, 0x58, 0x44, 0xa7, 0xce, 0x97, 0xdf, 0x80, 0x75, 0x11, 0x5e, 0xb9, 0x53, 0xb7, 0x30, 0x81, + 0xc3, 0x3a, 0x94, 0x0e, 0x0c, 0xb3, 0xb6, 0x77, 0xdf, 0xde, 0xeb, 0xd6, 0xeb, 0xea, 0x19, 0xb2, + 0x0a, 0xcb, 0x1c, 0x50, 0xd1, 0x55, 0x85, 0xac, 0x40, 0xb1, 0xd6, 0xb4, 0x8c, 0x4a, 0xd7, 0x34, + 0xd4, 0x5c, 0xf9, 0x0d, 0x58, 0x6b, 0x8f, 0xbd, 0xb7, 0x9c, 0x89, 0x7b, 0xd7, 0x7d, 0x84, 0xaf, + 0x51, 0x4b, 0x90, 0x37, 0xf5, 0x7b, 0xea, 0x19, 0x02, 0xb0, 0xd8, 0xbe, 0x5b, 0xb1, 0x6e, 0xdd, + 0x52, 0x15, 0x52, 0x82, 0xa5, 0xfd, 0x4a, 0xdb, 0xbe, 0xdb, 0xb0, 0xd4, 0x1c, 0xfd, 0xa1, 0xdf, + 0xb3, 0xf0, 0x47, 0xbe, 0xfc, 0x61, 0xd8, 0x40, 0xa9, 0xab, 0xee, 0x05, 0x13, 0x77, 0xe8, 0x8e, + 0xb1, 0x0e, 0x2b, 0x50, 0xb4, 0x5c, 0xba, 0x5d, 0x4e, 0x5c, 0x56, 0x81, 0xc6, 0x74, 0x30, 0xf1, + 0x46, 0x03, 0xf7, 0x2b, 0xaa, 0x52, 0x7e, 0x0d, 0xd6, 0x4d, 0x7f, 0x3a, 0xf1, 0x86, 0x47, 0xd6, + 0x84, 0x62, 0x1c, 0x3d, 0x22, 0xe7, 0x61, 0xa3, 0xdb, 0xd4, 0x1b, 0x3b, 0xb5, 0xfd, 0x6e, 0xab, + 0x6b, 0xd9, 0x0d, 0xbd, 0x53, 0xa9, 0xb2, 0xb7, 0xb0, 0x46, 0xcb, 0xea, 0xd8, 0xa6, 0x51, 0x31, + 0x9a, 0x1d, 0x55, 0x29, 0x7f, 0x0b, 0x15, 0x48, 0x3d, 0x7f, 0xd8, 0xdf, 0x73, 0x7a, 0x13, 0x7f, + 0x8c, 0x15, 0xd6, 0xe0, 0xb2, 0x65, 0x54, 0x5a, 0xcd, 0x5d, 0x7b, 0x4f, 0xaf, 0x74, 0x5a, 0x66, + 0x56, 0x3c, 0xf3, 0x6d, 0xb8, 0x90, 0x81, 0xd3, 0xea, 0xb4, 0x55, 0x85, 0x5c, 0x81, 0x8b, 0x19, + 0x65, 0xf7, 0x8c, 0x1d, 0xbd, 0xdb, 0xa9, 0x36, 0xd5, 0xdc, 0x0c, 0x62, 0xcb, 0x6a, 0xa9, 0xf9, + 0xf2, 0x6f, 0x2b, 0xb0, 0xd6, 0x0d, 0xb8, 0x5d, 0x7d, 0x17, 0x5d, 0x6a, 0x9f, 0x81, 0x4b, 0x5d, + 0xcb, 0x30, 0xed, 0x4e, 0xeb, 0xae, 0xd1, 0xb4, 0xbb, 0x96, 0xbe, 0x9f, 0xac, 0xcd, 0x15, 0xb8, + 0x28, 0x61, 0x98, 0x46, 0xa5, 0x75, 0x60, 0x98, 0x76, 0x5b, 0xb7, 0xac, 0x7b, 0x2d, 0x73, 0x57, + 0x55, 0xe8, 0x17, 0x33, 0x10, 0x1a, 0x7b, 0x3a, 0xab, 0x4d, 0xac, 0xac, 0x69, 0xdc, 0xd3, 0xeb, + 0xf6, 0x4e, 0xab, 0xa3, 0xe6, 0xcb, 0x0d, 0x2a, 0xc4, 0x60, 0x54, 0x61, 0x66, 0x3e, 0x59, 0x84, + 0x42, 0xb3, 0xd5, 0x34, 0x92, 0x2f, 0xa8, 0x2b, 0x50, 0xd4, 0xdb, 0x6d, 0xb3, 0x75, 0x80, 0x53, + 0x0c, 0x60, 0x71, 0xd7, 0x68, 0xd2, 0x9a, 0xe5, 0x69, 0x49, 0xdb, 0x6c, 0x35, 0x5a, 0x1d, 0x63, + 0x57, 0x2d, 0x94, 0x4d, 0xb1, 0xbf, 0x08, 0xa6, 0x3d, 0x9f, 0x3d, 0x57, 0xee, 0x1a, 0x7b, 0x7a, + 0xb7, 0xde, 0xe1, 0x43, 0x74, 0xdf, 0x36, 0x8d, 0x4f, 0x75, 0x0d, 0xab, 0x63, 0xa9, 0x0a, 0x51, + 0x61, 0xa5, 0x69, 0x18, 0xbb, 0x96, 0x6d, 0x1a, 0x07, 0x35, 0xe3, 0x9e, 0x9a, 0xa3, 0x3c, 0xd9, + 0xff, 0xf4, 0x0b, 0xe5, 0xef, 0x29, 0x40, 0x58, 0x44, 0x66, 0x91, 0xe6, 0x07, 0x67, 0xcc, 0x65, + 0xd8, 0xae, 0xd2, 0xa1, 0xc6, 0xa6, 0x35, 0x5a, 0xbb, 0xc9, 0x2e, 0xbb, 0x00, 0x24, 0x51, 0xde, + 0xda, 0xdb, 0x53, 0x15, 0x72, 0x11, 0xce, 0x26, 0xe0, 0xbb, 0x66, 0xab, 0xad, 0xe6, 0xb6, 0x73, + 0x45, 0x85, 0x6c, 0xa6, 0x0a, 0xef, 0x1a, 0x46, 0x5b, 0xcd, 0xd3, 0x21, 0x4a, 0x14, 0x88, 0x25, + 0xc1, 0xc8, 0x0b, 0xe5, 0x6f, 0x28, 0x70, 0x81, 0x55, 0x53, 0xac, 0xaf, 0xb0, 0xaa, 0x97, 0x60, + 0x8b, 0xc7, 0x99, 0xcf, 0xaa, 0xe8, 0x39, 0x50, 0x63, 0xa5, 0xac, 0x9a, 0xe7, 0x61, 0x23, 0x06, + 0xc5, 0x7a, 0xe4, 0xe8, 0xee, 0x11, 0x03, 0xef, 0x18, 0x56, 0xc7, 0x36, 0xf6, 0xf6, 0x5a, 0x66, + 0x87, 0x55, 0x24, 0x5f, 0xd6, 0x60, 0xa3, 0xe2, 0x8e, 0x27, 0xf4, 0x7e, 0x39, 0x0c, 0x3c, 0x7f, + 0x88, 0x55, 0x58, 0x85, 0x65, 0xe3, 0xd3, 0x1d, 0xa3, 0x69, 0xd5, 0x5a, 0x4d, 0xf5, 0x4c, 0xf9, + 0x52, 0x02, 0x47, 0xac, 0x63, 0xcb, 0xaa, 0xaa, 0x67, 0xca, 0x0e, 0xac, 0x0a, 0xeb, 0x72, 0x36, + 0x2b, 0x2e, 0xc3, 0xb6, 0x98, 0x6b, 0xb8, 0xa3, 0x24, 0x9b, 0xb0, 0x05, 0xe7, 0xd2, 0xe5, 0x46, + 0x47, 0x55, 0xe8, 0x28, 0x24, 0x4a, 0x28, 0x3c, 0x57, 0xfe, 0x4d, 0x05, 0x56, 0xc3, 0x97, 0x21, + 0xd4, 0x45, 0x5f, 0x81, 0x8b, 0x8d, 0x3d, 0xdd, 0xde, 0x35, 0x0e, 0x6a, 0x15, 0xc3, 0xbe, 0x5b, + 0x6b, 0xee, 0x26, 0x3e, 0xf2, 0x14, 0x9c, 0xcf, 0x40, 0xc0, 0xaf, 0x6c, 0xc1, 0xb9, 0x64, 0x51, + 0x87, 0x2e, 0xd5, 0x1c, 0xed, 0xfa, 0x64, 0x49, 0xb8, 0x4e, 0xf3, 0xe5, 0x03, 0x58, 0xb3, 0xf4, + 0x46, 0x7d, 0xcf, 0x1f, 0xf7, 0x5c, 0x7d, 0x3a, 0x39, 0x1e, 0x92, 0x8b, 0xb0, 0xb9, 0xd7, 0x32, + 0x2b, 0x86, 0x8d, 0x28, 0x89, 0x1a, 0x9c, 0x85, 0x75, 0xb9, 0xf0, 0xbe, 0x41, 0xa7, 0x2f, 0x81, + 0x35, 0x19, 0xd8, 0x6c, 0xa9, 0xb9, 0xf2, 0x67, 0x61, 0x25, 0x96, 0xed, 0x6f, 0x13, 0xce, 0xca, + 0xbf, 0xdb, 0xee, 0xb0, 0xef, 0x0d, 0x8f, 0xd4, 0x33, 0xc9, 0x02, 0x73, 0x3a, 0x1c, 0xd2, 0x02, + 0x5c, 0xcf, 0x72, 0x41, 0xc7, 0x1d, 0x9f, 0x78, 0x43, 0x67, 0xe2, 0xf6, 0xd5, 0x5c, 0xf9, 0x45, + 0x58, 0x8d, 0xc5, 0x18, 0xa7, 0x03, 0x57, 0x6f, 0xf1, 0x0d, 0xb8, 0x61, 0xec, 0xd6, 0xba, 0x0d, + 0x75, 0x81, 0xae, 0xe4, 0x6a, 0x6d, 0xbf, 0xaa, 0x42, 0xf9, 0xdb, 0x0a, 0xbd, 0x4c, 0x61, 0xe6, + 0xa0, 0xc6, 0x9e, 0x2e, 0x86, 0x9a, 0x4e, 0x33, 0x96, 0xb9, 0xc0, 0xb0, 0x2c, 0x66, 0x38, 0x70, + 0x09, 0xb6, 0xf8, 0x0f, 0x5b, 0x6f, 0xee, 0xda, 0x55, 0xdd, 0xdc, 0xbd, 0xa7, 0x9b, 0x74, 0xee, + 0xdd, 0x57, 0x73, 0xb8, 0xa0, 0x24, 0x88, 0xdd, 0x69, 0x75, 0x2b, 0x55, 0x35, 0x4f, 0xe7, 0x6f, + 0x0c, 0xde, 0xae, 0x35, 0xd5, 0x02, 0x2e, 0xcf, 0x14, 0x36, 0xb2, 0xa5, 0xe5, 0x0b, 0xe5, 0x77, + 0x15, 0xd8, 0xb4, 0xbc, 0xa3, 0xa1, 0x33, 0x99, 0x8e, 0x5d, 0x7d, 0x70, 0xe4, 0x8f, 0xbd, 0xc9, + 0xf1, 0x89, 0x35, 0xf5, 0x26, 0x2e, 0xb9, 0x09, 0xcf, 0x59, 0xb5, 0xfd, 0xa6, 0xde, 0xa1, 0xcb, + 0x4b, 0xaf, 0xef, 0xb7, 0xcc, 0x5a, 0xa7, 0xda, 0xb0, 0xad, 0x6e, 0x2d, 0x35, 0xf3, 0xae, 0xc1, + 0x33, 0xb3, 0x51, 0xeb, 0xc6, 0xbe, 0x5e, 0xb9, 0xaf, 0x2a, 0xf3, 0x19, 0xee, 0xe8, 0x75, 0xbd, + 0x59, 0x31, 0x76, 0xed, 0x83, 0x5b, 0x6a, 0x8e, 0x3c, 0x07, 0x57, 0x67, 0xa3, 0xee, 0xd5, 0xda, + 0x16, 0x45, 0xcb, 0xcf, 0xff, 0x6e, 0xd5, 0x6a, 0x50, 0xac, 0x42, 0xf9, 0x3b, 0x0a, 0x6c, 0xcd, + 0x0a, 0x34, 0x45, 0xae, 0x83, 0x66, 0x34, 0x3b, 0xa6, 0x5e, 0xdb, 0xb5, 0x2b, 0xa6, 0xb1, 0x6b, + 0x34, 0x3b, 0x35, 0xbd, 0x6e, 0xd9, 0x56, 0xab, 0x4b, 0x67, 0x53, 0x64, 0xdf, 0xf1, 0x2c, 0x5c, + 0x99, 0x83, 0xd7, 0xaa, 0xed, 0x56, 0x54, 0x85, 0xdc, 0x82, 0x17, 0xe6, 0x20, 0x59, 0xf7, 0xad, + 0x8e, 0xd1, 0x90, 0x4b, 0xd4, 0x5c, 0xb9, 0x02, 0xdb, 0xb3, 0x23, 0xd1, 0xd0, 0x6d, 0x3a, 0xde, + 0xd3, 0x45, 0x28, 0xec, 0xd2, 0x93, 0x21, 0x96, 0xe0, 0xa2, 0xec, 0x81, 0x9a, 0x0c, 0x26, 0x91, + 0x32, 0xc4, 0x31, 0xbb, 0xcd, 0x26, 0x3b, 0x46, 0xd6, 0xa1, 0xd4, 0xea, 0x54, 0x0d, 0x93, 0xa7, + 0x08, 0xc1, 0x9c, 0x20, 0xdd, 0x26, 0x5d, 0x38, 0x2d, 0xb3, 0xf6, 0x19, 0x3c, 0x4f, 0xb6, 0xe0, + 0x9c, 0x55, 0xd7, 0x2b, 0x77, 0xed, 0x66, 0xab, 0x63, 0xd7, 0x9a, 0x76, 0xa5, 0xaa, 0x37, 0x9b, + 0x46, 0x5d, 0x05, 0xec, 0xcc, 0x59, 0x0e, 0xa4, 0xe4, 0x83, 0x70, 0xa3, 0x75, 0xb7, 0xa3, 0xdb, + 0xed, 0x7a, 0x77, 0xbf, 0xd6, 0xb4, 0xad, 0xfb, 0xcd, 0x8a, 0x90, 0x7d, 0x2a, 0xe9, 0x2d, 0xf7, + 0x06, 0x5c, 0x9b, 0x8b, 0x1d, 0x25, 0xf3, 0xb8, 0x0e, 0xda, 0x5c, 0x4c, 0xde, 0x90, 0xf2, 0x8f, + 0x15, 0xb8, 0x38, 0xe7, 0xa1, 0x9c, 0xbc, 0x00, 0x37, 0xab, 0x86, 0xbe, 0x5b, 0x37, 0x2c, 0x0b, + 0x37, 0x0a, 0x3a, 0x0c, 0xcc, 0x60, 0x27, 0x73, 0x43, 0xbd, 0x09, 0xcf, 0xcd, 0x47, 0x8f, 0x8e, + 0xe6, 0x1b, 0x70, 0x6d, 0x3e, 0x2a, 0x3f, 0xaa, 0x73, 0xa4, 0x0c, 0xd7, 0xe7, 0x63, 0x86, 0x47, + 0x7c, 0xbe, 0xfc, 0xbb, 0x0a, 0x5c, 0xc8, 0xd6, 0x56, 0xd1, 0xba, 0xd5, 0x9a, 0x56, 0x47, 0xaf, + 0xd7, 0xed, 0xb6, 0x6e, 0xea, 0x0d, 0xdb, 0x68, 0x9a, 0xad, 0x7a, 0x3d, 0xeb, 0x68, 0xbb, 0x06, + 0xcf, 0xcc, 0x46, 0xb5, 0x2a, 0x66, 0xad, 0x4d, 0x77, 0x6f, 0x0d, 0x2e, 0xcf, 0xc6, 0x32, 0x6a, + 0x15, 0x43, 0xcd, 0xed, 0xbc, 0xfe, 0xc3, 0x7f, 0xbc, 0x7c, 0xe6, 0x87, 0xef, 0x5e, 0x56, 0x7e, + 0xf2, 0xee, 0x65, 0xe5, 0x1f, 0xde, 0xbd, 0xac, 0x7c, 0xe6, 0xf9, 0xd3, 0xe5, 0xc1, 0xc2, 0x4b, + 0xc9, 0x83, 0x45, 0xbc, 0x86, 0xbd, 0xf4, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x64, 0x0f, 0xaf, + 0x27, 0x13, 0xc0, 0x01, 0x00, } func (this *PluginSpecV1) Equal(that interface{}) bool { @@ -50363,6 +50413,20 @@ func (m *AccessGraphSync) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } + if len(m.Azure) > 0 { + for iNdEx := len(m.Azure) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Azure[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } n432, err432 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.PollInterval, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.PollInterval):]) if err432 != nil { return 0, err432 @@ -50443,6 +50507,47 @@ func (m *AccessGraphAWSSync) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *AccessGraphAzureSync) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *AccessGraphAzureSync) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *AccessGraphAzureSync) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if len(m.Integration) > 0 { + i -= len(m.Integration) + copy(dAtA[i:], m.Integration) + i = encodeVarintTypes(dAtA, i, uint64(len(m.Integration))) + i-- + dAtA[i] = 0x12 + } + if len(m.SubscriptionID) > 0 { + i -= len(m.SubscriptionID) + copy(dAtA[i:], m.SubscriptionID) + i = encodeVarintTypes(dAtA, i, uint64(len(m.SubscriptionID))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintTypes(dAtA []byte, offset int, v uint64) int { offset -= sovTypes(v) base := offset @@ -61808,6 +61913,12 @@ func (m *AccessGraphSync) Size() (n int) { } l = github_com_gogo_protobuf_types.SizeOfStdDuration(m.PollInterval) n += 1 + l + sovTypes(uint64(l)) + if len(m.Azure) > 0 { + for _, e := range m.Azure { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -61840,6 +61951,26 @@ func (m *AccessGraphAWSSync) Size() (n int) { return n } +func (m *AccessGraphAzureSync) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.SubscriptionID) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } + l = len(m.Integration) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + func sovTypes(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -134181,6 +134312,40 @@ func (m *AccessGraphSync) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Azure", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Azure = append(m.Azure, &AccessGraphAzureSync{}) + if err := m.Azure[len(m.Azure)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTypes(dAtA[iNdEx:]) @@ -134354,6 +134519,121 @@ func (m *AccessGraphAWSSync) Unmarshal(dAtA []byte) error { } return nil } +func (m *AccessGraphAzureSync) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: AccessGraphAzureSync: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: AccessGraphAzureSync: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SubscriptionID", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.SubscriptionID = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Integration", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Integration = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipTypes(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/gen/proto/go/accessgraph/v1alpha/azure.pb.go b/gen/proto/go/accessgraph/v1alpha/azure.pb.go index 399c97bd464e2..e2f78bee99f2a 100644 --- a/gen/proto/go/accessgraph/v1alpha/azure.pb.go +++ b/gen/proto/go/accessgraph/v1alpha/azure.pb.go @@ -684,7 +684,7 @@ func (x *AzureRoleDefinition) GetType() string { return "" } -// AzurePermission defines the actions and not (disallowed) actions for a role definition +// AzureRBACPermission defines the actions and not (disallowed) actions for a role definition type AzureRBACPermission struct { state protoimpl.MessageState `protogen:"open.v1"` // actions define the resources and verbs allowed on the resources diff --git a/lib/config/configuration.go b/lib/config/configuration.go index 54dc6a2c82820..929bb757a85ca 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -1754,6 +1754,12 @@ kubernetes matchers are present`) AssumeRole: assumeRole, }) } + for _, azureMatcher := range fc.Discovery.AccessGraph.Azure { + subscriptionID := azureMatcher.SubscriptionID + tMatcher.Azure = append(tMatcher.Azure, &types.AccessGraphAzureSync{ + SubscriptionID: subscriptionID, + }) + } if fc.Discovery.AccessGraph.PollInterval > 0 { tMatcher.PollInterval = fc.Discovery.AccessGraph.PollInterval } diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go index a4bc53787ce9f..0c417106924e7 100644 --- a/lib/config/fileconf.go +++ b/lib/config/fileconf.go @@ -1523,6 +1523,8 @@ type GCPMatcher struct { type AccessGraphSync struct { // AWS is the AWS configuration for the AccessGraph Sync service. AWS []AccessGraphAWSSync `yaml:"aws,omitempty"` + // Azure is the Azure configuration for the AccessGraph Sync service. + Azure []AccessGraphAzureSync `yaml:"azure,omitempty"` // PollInterval is the frequency at which to poll for AWS resources PollInterval time.Duration `yaml:"poll_interval,omitempty"` } @@ -1538,6 +1540,12 @@ type AccessGraphAWSSync struct { ExternalID string `yaml:"external_id,omitempty"` } +// AccessGraphAzureSync represents the configuration for the Azure AccessGraph Sync service. +type AccessGraphAzureSync struct { + // SubscriptionID is the Azure subscription ID configured for syncing + SubscriptionID string `yaml:"subscription_id,omitempty"` +} + // CommandLabel is `command` section of `ssh_service` in the config file type CommandLabel struct { Name string `yaml:"name"` diff --git a/proto/accessgraph/v1alpha/azure.proto b/proto/accessgraph/v1alpha/azure.proto index 1050c3c98f75e..58bef9b36e97b 100644 --- a/proto/accessgraph/v1alpha/azure.proto +++ b/proto/accessgraph/v1alpha/azure.proto @@ -127,7 +127,7 @@ message AzureRoleDefinition { string type = 9; } -// AzurePermission defines the actions and not (disallowed) actions for a role definition +// AzureRBACPermission defines the actions and not (disallowed) actions for a role definition message AzureRBACPermission { // actions define the resources and verbs allowed on the resources repeated string actions = 1; From 1345dbfe102480d1bd27527a42a4607a5dca1b44 Mon Sep 17 00:00:00 2001 From: Zac Bergquist Date: Wed, 18 Dec 2024 13:49:07 -0700 Subject: [PATCH 10/64] Make sure progress bar stops moving when pausing a desktop recording (#50371) Closes #42467 --- web/packages/teleport/src/lib/tdp/playerClient.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/packages/teleport/src/lib/tdp/playerClient.ts b/web/packages/teleport/src/lib/tdp/playerClient.ts index dd32996b5bd79..9adb15ef10038 100644 --- a/web/packages/teleport/src/lib/tdp/playerClient.ts +++ b/web/packages/teleport/src/lib/tdp/playerClient.ts @@ -112,6 +112,11 @@ export class PlayerClient extends Client { } this.lastUpdateTime = Date.now(); + this.send(JSON.stringify({ action: Action.TOGGLE_PLAY_PAUSE })); + + if (this.paused) { + return; + } if (this.isSeekingForward()) { const next = Math.max(this.skipTimeUpdatesUntil, this.lastTimestamp); @@ -119,8 +124,6 @@ export class PlayerClient extends Client { } else { this.scheduleNextUpdate(this.lastTimestamp); } - - this.send(JSON.stringify({ action: Action.TOGGLE_PLAY_PAUSE })); } // setPlaySpeed sets the playback speed of the recording. From c222030e3f2eff643d90d30f4b634482fa896ba2 Mon Sep 17 00:00:00 2001 From: Zac Bergquist Date: Wed, 18 Dec 2024 15:08:17 -0700 Subject: [PATCH 11/64] Remove CSRF checking middleware (#50358) The remaining two endpoints that were checking the CSRF token were both unauthenticated requests. We don't need a CSRF token here because we require Content-Type: application/json for these requests. --- lib/httplib/httplib.go | 19 +---------- lib/web/apiserver.go | 29 ++-------------- lib/web/apiserver_test.go | 64 +++++++----------------------------- lib/web/login_helper_test.go | 22 +++---------- 4 files changed, 18 insertions(+), 116 deletions(-) diff --git a/lib/httplib/httplib.go b/lib/httplib/httplib.go index 98775ec69040c..f241f6d36ddb8 100644 --- a/lib/httplib/httplib.go +++ b/lib/httplib/httplib.go @@ -41,7 +41,6 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/observability/tracing" tracehttp "github.com/gravitational/teleport/api/observability/tracing/http" - "github.com/gravitational/teleport/lib/httplib/csrf" "github.com/gravitational/teleport/lib/utils" ) @@ -155,23 +154,6 @@ func MakeStdHandlerWithErrorWriter(fn StdHandlerFunc, errWriter ErrorWriter) htt } } -// WithCSRFProtection ensures that request to unauthenticated API is checked against CSRF attacks -func WithCSRFProtection(fn HandlerFunc) httprouter.Handle { - handlerFn := MakeHandler(fn) - return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - if r.Method != http.MethodGet && r.Method != http.MethodHead { - errHeader := csrf.VerifyHTTPHeader(r) - errForm := csrf.VerifyFormField(r) - if errForm != nil && errHeader != nil { - slog.WarnContext(r.Context(), "unable to validate CSRF token", "header_error", errHeader, "form_error", errForm) - trace.WriteError(w, trace.AccessDenied("access denied")) - return - } - } - handlerFn(w, r, p) - } -} - // ReadJSON reads HTTP json request and unmarshals it // into passed any obj. A reasonable maximum size is enforced // to mitigate resource exhaustion attacks. @@ -188,6 +170,7 @@ func ReadResourceJSON(r *http.Request, val any) error { func readJSON(r *http.Request, val any, maxSize int64) error { // Check content type to mitigate CSRF attack. + // (Form POST requests don't support application/json payloads.) contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil { slog.WarnContext(r.Context(), "Error parsing media type for reading JSON", "error", err) diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index a21d165e382de..b88f4c0102edf 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -780,7 +780,7 @@ func (h *Handler) bindDefaultEndpoints() { h.POST("/webapi/sessions/app", h.WithAuth(h.createAppSession)) // Web sessions - h.POST("/webapi/sessions/web", httplib.WithCSRFProtection(h.WithLimiterHandlerFunc(h.createWebSession))) + h.POST("/webapi/sessions/web", h.WithLimiter(h.createWebSession)) h.DELETE("/webapi/sessions/web", h.WithAuth(h.deleteWebSession)) h.POST("/webapi/sessions/web/renew", h.WithAuth(h.renewWebSession)) h.POST("/webapi/users", h.WithAuth(h.createUserHandle)) @@ -793,7 +793,7 @@ func (h *Handler) bindDefaultEndpoints() { // h.GET("/webapi/users/password/token/:token", h.WithLimiter(h.getResetPasswordTokenHandle)) h.GET("/webapi/users/*wildcard", h.handleGetUserOrResetToken) - h.PUT("/webapi/users/password/token", httplib.WithCSRFProtection(h.changeUserAuthentication)) + h.PUT("/webapi/users/password/token", h.WithLimiter(h.changeUserAuthentication)) h.PUT("/webapi/users/password", h.WithAuth(h.changePassword)) h.POST("/webapi/users/password/token", h.WithAuth(h.createResetPasswordToken)) h.POST("/webapi/users/privilege/token", h.WithAuth(h.createPrivilegeTokenHandle)) @@ -1994,7 +1994,6 @@ func (h *Handler) githubLoginWeb(w http.ResponseWriter, r *http.Request, p httpr } response, err := h.cfg.ProxyClient.CreateGithubAuthRequest(r.Context(), types.GithubAuthRequest{ - CSRFToken: req.CSRFToken, ConnectorID: req.ConnectorID, CreateWebSession: true, ClientRedirectURL: req.ClientRedirectURL, @@ -2004,7 +2003,6 @@ func (h *Handler) githubLoginWeb(w http.ResponseWriter, r *http.Request, p httpr if err != nil { logger.WithError(err).Error("Error creating auth request.") return client.LoginFailedRedirectURL - } return response.RedirectURL @@ -4705,21 +4703,6 @@ func (h *Handler) WithSession(fn ContextHandler) httprouter.Handle { }) } -// WithAuthCookieAndCSRF ensures that a request is authenticated -// for plain old non-AJAX requests (does not check the Bearer header). -// It enforces CSRF checks (except for "safe" methods). -func (h *Handler) WithAuthCookieAndCSRF(fn ContextHandler) httprouter.Handle { - f := func(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { - sctx, err := h.AuthenticateRequest(w, r, false) - if err != nil { - return nil, trace.Wrap(err) - } - return fn(w, r, p, sctx) - } - - return httplib.WithCSRFProtection(f) -} - // WithUnauthenticatedLimiter adds a conditional IP-based rate limiting that will limit only unauthenticated requests. // This is a good default to use as both Cluster and User auth are checked here, but `WithLimiter` can be used if // you're certain that no authenticated requests will be made. @@ -5054,8 +5037,6 @@ type SSORequestParams struct { // ConnectorID identifies the SSO connector to use to log in, from // the connector_id query parameter. ConnectorID string - // CSRFToken is the token in the CSRF cookie header. - CSRFToken string } // ParseSSORequestParams extracts the SSO request parameters from an http.Request, @@ -5088,15 +5069,9 @@ func ParseSSORequestParams(r *http.Request) (*SSORequestParams, error) { return nil, trace.BadParameter("missing connector_id query parameter") } - csrfToken, err := csrf.ExtractTokenFromCookie(r) - if err != nil { - return nil, trace.Wrap(err) - } - return &SSORequestParams{ ClientRedirectURL: clientRedirectURL, ConnectorID: connectorID, - CSRFToken: csrfToken, }, nil } diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index 962d52514cad9..48a9ff61179a5 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -117,7 +117,6 @@ import ( "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/events/eventstest" "github.com/gravitational/teleport/lib/httplib" - "github.com/gravitational/teleport/lib/httplib/csrf" "github.com/gravitational/teleport/lib/inventory" kubeproxy "github.com/gravitational/teleport/lib/kube/proxy" "github.com/gravitational/teleport/lib/limiter" @@ -947,10 +946,6 @@ func TestWebSessionsCRUD(t *testing.T) { func TestCSRF(t *testing.T) { t.Parallel() s := newWebSuite(t) - type input struct { - reqToken string - cookieToken string - } // create a valid user user := "csrfuser" @@ -958,39 +953,25 @@ func TestCSRF(t *testing.T) { otpSecret := newOTPSharedSecret() s.createUser(t, user, user, pass, otpSecret) - encodedToken1 := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992" - encodedToken2 := "bf355921bbf3ef3672a03e410d4194077dfa5fe863c652521763b3e7f81e7b11" - invalid := []input{ - {reqToken: encodedToken2, cookieToken: encodedToken1}, - {reqToken: "", cookieToken: encodedToken1}, - {reqToken: "", cookieToken: ""}, - {reqToken: encodedToken1, cookieToken: ""}, - } - clt := s.client(t) ctx := context.Background() // valid validReq := loginWebOTPParams{ - webClient: clt, - clock: s.clock, - user: user, - password: pass, - otpSecret: otpSecret, - cookieCSRF: &encodedToken1, - headerCSRF: &encodedToken1, + webClient: clt, + clock: s.clock, + user: user, + password: pass, + otpSecret: otpSecret, } loginWebOTP(t, ctx, validReq) - // invalid - for i := range invalid { - req := validReq - req.cookieCSRF = &invalid[i].cookieToken - req.headerCSRF = &invalid[i].reqToken - httpResp, _, err := rawLoginWebOTP(ctx, req) - require.NoError(t, err, "Login via /webapi/sessions/new failed unexpectedly") - assert.Equal(t, http.StatusForbidden, httpResp.StatusCode, "HTTP status code mismatch") - } + // invalid - wrong content-type header + invalidReq := validReq + invalidReq.overrideContentType = "multipart/form-data" + httpResp, _, err := rawLoginWebOTP(ctx, invalidReq) + require.NoError(t, err, "Login via /webapi/sessions/new failed unexpectedly") + require.Equal(t, http.StatusBadRequest, httpResp.StatusCode, "HTTP status code mismatch") } func TestPasswordChange(t *testing.T) { @@ -5953,13 +5934,9 @@ func TestChangeUserAuthentication_WithPrivacyPolicyEnabledError(t *testing.T) { httpReqData, err := json.Marshal(req) require.NoError(t, err) - // CSRF protected endpoint. - csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992" httpReq, err := http.NewRequest("PUT", clt.Endpoint("webapi", "users", "password", "token"), bytes.NewBuffer(httpReqData)) require.NoError(t, err) - addCSRFCookieToReq(httpReq, csrfToken) httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set(csrf.HeaderName, csrfToken) httpRes, err := httplib.ConvertResponse(clt.RoundTrip(func() (*http.Response, error) { return clt.HTTPClient().Do(httpReq) })) @@ -6104,10 +6081,6 @@ func TestChangeUserAuthentication_settingDefaultClusterAuthPreference(t *testing req, err := http.NewRequest("PUT", clt.Endpoint("webapi", "users", "password", "token"), bytes.NewBuffer(body)) require.NoError(t, err) - csrfToken, err := csrf.GenerateToken() - require.NoError(t, err) - addCSRFCookieToReq(req, csrfToken) - req.Header.Set(csrf.HeaderName, csrfToken) req.Header.Set("Content-Type", "application/json") re, err := clt.Client.RoundTrip(func() (*http.Response, error) { @@ -6129,8 +6102,6 @@ func TestChangeUserAuthentication_settingDefaultClusterAuthPreference(t *testing func TestParseSSORequestParams(t *testing.T) { t.Parallel() - token := "someMeaninglessTokenString" - tests := []struct { name, url string wantErr bool @@ -6142,7 +6113,6 @@ func TestParseSSORequestParams(t *testing.T) { expected: &SSORequestParams{ ClientRedirectURL: "https://localhost:8080/web/cluster/im-a-cluster-name/nodes?search=tunnel&sort=hostname:asc", ConnectorID: "oidc", - CSRFToken: token, }, }, { @@ -6151,7 +6121,6 @@ func TestParseSSORequestParams(t *testing.T) { expected: &SSORequestParams{ ClientRedirectURL: "https://localhost:8080/web/cluster/im-a-cluster-name/nodes?search=tunnel&sort=hostname:asc", ConnectorID: "github", - CSRFToken: token, }, }, { @@ -6160,7 +6129,6 @@ func TestParseSSORequestParams(t *testing.T) { expected: &SSORequestParams{ ClientRedirectURL: "https://localhost:8080/web/cluster/im-a-cluster-name/apps?query=search(%22watermelon%22%2C%20%22this%22)%20%26%26%20labels%5B%22unique-id%22%5D%20%3D%3D%20%22hi%22&sort=name:asc", ConnectorID: "saml", - CSRFToken: token, }, }, { @@ -6179,7 +6147,6 @@ func TestParseSSORequestParams(t *testing.T) { t.Run(tc.name, func(t *testing.T) { req, err := http.NewRequest("", tc.url, nil) require.NoError(t, err) - addCSRFCookieToReq(req, token) params, err := ParseSSORequestParams(req) @@ -7932,15 +7899,6 @@ func (s *WebSuite) url() *url.URL { return u } -func addCSRFCookieToReq(req *http.Request, token string) { - cookie := &http.Cookie{ - Name: csrf.CookieName, - Value: token, - } - - req.AddCookie(cookie) -} - func removeSpace(in string) string { for _, c := range []string{"\n", "\r", "\t"} { in = strings.Replace(in, c, " ", -1) diff --git a/lib/web/login_helper_test.go b/lib/web/login_helper_test.go index ae7f2ddbd93c8..2829a2d1400d7 100644 --- a/lib/web/login_helper_test.go +++ b/lib/web/login_helper_test.go @@ -18,6 +18,7 @@ package web import ( "bytes" + "cmp" "context" "encoding/base32" "encoding/json" @@ -34,7 +35,6 @@ import ( "github.com/gravitational/teleport/lib/auth/mocku2f" "github.com/gravitational/teleport/lib/client" - "github.com/gravitational/teleport/lib/httplib/csrf" ) // newOTPSharedSecret returns an OTP shared secret, encoded as a base32 string. @@ -54,9 +54,8 @@ type loginWebOTPParams struct { // If empty then no OTP is sent in the request. otpSecret string - userAgent string // Optional. - - cookieCSRF, headerCSRF *string // Explicit CSRF tokens. Optional. + userAgent string // Optional. + overrideContentType string // Optional. } // DrainedHTTPResponse mimics an http.Response, but without a body. @@ -124,24 +123,11 @@ func rawLoginWebOTP(ctx context.Context, params loginWebOTPParams) (resp *Draine } // Set assorted headers. - req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Type", cmp.Or(params.overrideContentType, "application/json")) if params.userAgent != "" { req.Header.Set("User-Agent", params.userAgent) } - // Set CSRF cookie and header. - const defaultCSRFToken = "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992" - cookieCSRF := defaultCSRFToken - if params.cookieCSRF != nil { - cookieCSRF = *params.cookieCSRF - } - addCSRFCookieToReq(req, cookieCSRF) - headerCSRF := defaultCSRFToken - if params.headerCSRF != nil { - headerCSRF = *params.headerCSRF - } - req.Header.Set(csrf.HeaderName, headerCSRF) - httpResp, err := webClient.HTTPClient().Do(req) if err != nil { return nil, nil, trace.Wrap(err, "do HTTP request") From 759899fabfddad4d92558a47933c1a3cc653778d Mon Sep 17 00:00:00 2001 From: Edoardo Spadolini Date: Wed, 18 Dec 2024 23:36:00 +0100 Subject: [PATCH 12/64] Variable rate heartbeats (#49562) * Allow resetting the inventory delay * Upsert-only SSH heartbeats * Variable rate SSH heartbeats * Variable rate heartbeats for non-SSH resources * Add a helper method to create the HB delays --- lib/inventory/controller.go | 322 ++++++++++++++++---------- lib/inventory/controller_test.go | 31 ++- lib/inventory/internal/delay/delay.go | 20 +- 3 files changed, 250 insertions(+), 123 deletions(-) diff --git a/lib/inventory/controller.go b/lib/inventory/controller.go index 8ea733c950dc6..4bdbd2f596c41 100644 --- a/lib/inventory/controller.go +++ b/lib/inventory/controller.go @@ -36,6 +36,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/retryutils" "github.com/gravitational/teleport/lib/inventory/internal/delay" + "github.com/gravitational/teleport/lib/services" usagereporter "github.com/gravitational/teleport/lib/usagereporter/teleport" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/interval" @@ -118,11 +119,13 @@ const ( keepAliveKubeTick = "keep-alive-kube-tick" ) -// instanceHBStepSize is the step size used for the variable instance heartbeat duration. This value is -// basically arbitrary. It was selected because it produces a scaling curve that makes a fairly reasonable -// tradeoff between heartbeat availability and load scaling. See test coverage in the 'interval' package -// for a demonstration of the relationship between step sizes and interval/duration scaling. -const instanceHBStepSize = 1024 +// heartbeatStepSize is the step size used for the variable heartbeat intervals. +// This value is basically arbitrary. It was selected because it produces a +// scaling curve that makes a fairly reasonable tradeoff between heartbeat +// availability and load scaling. See test coverage in the 'interval' package +// for a demonstration of the relationship between step sizes and +// interval/duration scaling. +const heartbeatStepSize = 1024 type controllerOptions struct { serverKeepAlive time.Duration @@ -233,6 +236,10 @@ type Controller struct { instanceTTL time.Duration instanceHBEnabled bool instanceHBVariableDuration *interval.VariableDuration + sshHBVariableDuration *interval.VariableDuration + appHBVariableDuration *interval.VariableDuration + dbHBVariableDuration *interval.VariableDuration + kubeHBVariableDuration *interval.VariableDuration maxKeepAliveErrs int usageReporter usagereporter.UsageReporter testEvents chan testEvent @@ -254,18 +261,55 @@ func NewController(auth Auth, usageReporter usagereporter.UsageReporter, opts .. instanceHBVariableDuration := interval.NewVariableDuration(interval.VariableDurationConfig{ MinDuration: options.instanceHBInterval, MaxDuration: apidefaults.MaxInstanceHeartbeatInterval, - Step: instanceHBStepSize, + Step: heartbeatStepSize, }) + var ( + sshHBVariableDuration *interval.VariableDuration + appHBVariableDuration *interval.VariableDuration + dbHBVariableDuration *interval.VariableDuration + kubeHBVariableDuration *interval.VariableDuration + ) + serverTTL := apidefaults.ServerAnnounceTTL + if !variableRateHeartbeatsDisabledEnv() { + // by default, heartbeats will scale from 1.5 to 6 minutes, and will + // have a TTL of 15 minutes + serverTTL = apidefaults.ServerAnnounceTTL * 3 / 2 + sshHBVariableDuration = interval.NewVariableDuration(interval.VariableDurationConfig{ + MinDuration: options.serverKeepAlive, + MaxDuration: options.serverKeepAlive * 4, + Step: heartbeatStepSize, + }) + appHBVariableDuration = interval.NewVariableDuration(interval.VariableDurationConfig{ + MinDuration: options.serverKeepAlive, + MaxDuration: options.serverKeepAlive * 4, + Step: heartbeatStepSize, + }) + dbHBVariableDuration = interval.NewVariableDuration(interval.VariableDurationConfig{ + MinDuration: options.serverKeepAlive, + MaxDuration: options.serverKeepAlive * 4, + Step: heartbeatStepSize, + }) + kubeHBVariableDuration = interval.NewVariableDuration(interval.VariableDurationConfig{ + MinDuration: options.serverKeepAlive, + MaxDuration: options.serverKeepAlive * 4, + Step: heartbeatStepSize, + }) + } + ctx, cancel := context.WithCancel(context.Background()) return &Controller{ store: NewStore(), serviceCounter: &serviceCounter{}, serverKeepAlive: options.serverKeepAlive, - serverTTL: apidefaults.ServerAnnounceTTL, + serverTTL: serverTTL, instanceTTL: apidefaults.InstanceHeartbeatTTL, instanceHBEnabled: !instanceHeartbeatsDisabledEnv(), instanceHBVariableDuration: instanceHBVariableDuration, + sshHBVariableDuration: sshHBVariableDuration, + appHBVariableDuration: appHBVariableDuration, + dbHBVariableDuration: dbHBVariableDuration, + kubeHBVariableDuration: kubeHBVariableDuration, maxKeepAliveErrs: options.maxKeepAliveErrs, auth: auth, authID: options.authID, @@ -417,23 +461,36 @@ func (c *Controller) handleControlStream(handle *upstreamHandle) { if handle.sshServer != nil { c.onDisconnectFunc(constants.KeepAliveNode, 1) + if c.sshHBVariableDuration != nil { + c.sshHBVariableDuration.Dec() + } + handle.sshServer = nil } if len(handle.appServers) > 0 { c.onDisconnectFunc(constants.KeepAliveApp, len(handle.appServers)) + if c.appHBVariableDuration != nil { + c.appHBVariableDuration.Add(-len(handle.appServers)) + } + clear(handle.appServers) } if len(handle.databaseServers) > 0 { c.onDisconnectFunc(constants.KeepAliveDatabase, len(handle.databaseServers)) + if c.dbHBVariableDuration != nil { + c.dbHBVariableDuration.Add(-len(handle.databaseServers)) + } + clear(handle.databaseServers) } if len(handle.kubernetesServers) > 0 { c.onDisconnectFunc(constants.KeepAliveKube, len(handle.kubernetesServers)) + if c.kubeHBVariableDuration != nil { + c.kubeHBVariableDuration.Add(-len(handle.kubernetesServers)) + } + clear(handle.kubernetesServers) } - clear(handle.appServers) - clear(handle.databaseServers) - clear(handle.kubernetesServers) c.testEvent(handlerClose) }() @@ -448,40 +505,60 @@ func (c *Controller) handleControlStream(handle *upstreamHandle) { case proto.UpstreamInventoryAgentMetadata: c.handleAgentMetadata(handle, m) case proto.InventoryHeartbeat: - if err := c.handleHeartbeatMsg(handle, m); err != nil { - handle.CloseWithError(err) - return + // XXX: when adding new services to the heartbeat logic, make + // sure to also update the 'icsServiceToMetricName' mapping in + // auth/grpcserver.go in order to ensure that metrics start + // counting the control stream as a registered keepalive stream + // for that service. + + if m.SSHServer != nil { + // we initialize sshKeepAliveDelay before calling + // handleSSHServerHB unlike the other heartbeat types + // because handleSSHServerHB needs the delay to reset it + // after an announce, including the first one + if sshKeepAliveDelay == nil { + sshKeepAliveDelay = c.createKeepAliveDelay(c.sshHBVariableDuration) + } + + if err := c.handleSSHServerHB(handle, m.SSHServer, sshKeepAliveDelay); err != nil { + handle.CloseWithError(trace.Wrap(err)) + return + } } - // we initialize delays lazily here, depending on the protocol - if sshKeepAliveDelay == nil && m.SSHServer != nil { - sshKeepAliveDelay = delay.New(delay.Params{ - FirstInterval: retryutils.HalfJitter(c.serverKeepAlive), - FixedInterval: c.serverKeepAlive, - Jitter: retryutils.SeventhJitter, - }) - } - if appKeepAliveDelay == nil && m.AppServer != nil { - appKeepAliveDelay = delay.New(delay.Params{ - FirstInterval: retryutils.HalfJitter(c.serverKeepAlive), - FixedInterval: c.serverKeepAlive, - Jitter: retryutils.SeventhJitter, - }) + if m.AppServer != nil { + if err := c.handleAppServerHB(handle, m.AppServer); err != nil { + handle.CloseWithError(err) + return + } + + if appKeepAliveDelay == nil { + appKeepAliveDelay = c.createKeepAliveDelay(c.appHBVariableDuration) + } } - if dbKeepAliveDelay == nil && m.DatabaseServer != nil { - dbKeepAliveDelay = delay.New(delay.Params{ - FirstInterval: retryutils.HalfJitter(c.serverKeepAlive), - FixedInterval: c.serverKeepAlive, - Jitter: retryutils.SeventhJitter, - }) + + if m.DatabaseServer != nil { + if err := c.handleDatabaseServerHB(handle, m.DatabaseServer); err != nil { + handle.CloseWithError(err) + return + } + + if dbKeepAliveDelay == nil { + dbKeepAliveDelay = c.createKeepAliveDelay(c.dbHBVariableDuration) + } } - if kubeKeepAliveDelay == nil && m.KubernetesServer != nil { - kubeKeepAliveDelay = delay.New(delay.Params{ - FirstInterval: retryutils.HalfJitter(c.serverKeepAlive), - FixedInterval: c.serverKeepAlive, - Jitter: retryutils.SeventhJitter, - }) + + if m.KubernetesServer != nil { + if err := c.handleKubernetesServerHB(handle, m.KubernetesServer); err != nil { + handle.CloseWithError(err) + return + } + + if kubeKeepAliveDelay == nil { + kubeKeepAliveDelay = c.createKeepAliveDelay(c.kubeHBVariableDuration) + } } + case proto.UpstreamInventoryPong: c.handlePong(handle, m) case proto.UpstreamInventoryGoodbye: @@ -570,6 +647,12 @@ func instanceHeartbeatsDisabledEnv() bool { return os.Getenv("TELEPORT_UNSTABLE_DISABLE_INSTANCE_HB") == "yes" } +// variableRateHeartbeatsDisabledEnv checks if variable rate heartbeats have +// been explicitly disabled via environment variable. +func variableRateHeartbeatsDisabledEnv() bool { + return os.Getenv("TELEPORT_UNSTABLE_DISABLE_VARIABLE_RATE_HEARTBEATS") == "yes" +} + func (c *Controller) heartbeatInstanceState(handle *upstreamHandle, now time.Time) error { if !c.instanceHBEnabled { return nil @@ -673,39 +756,7 @@ func (c *Controller) handlePingRequest(handle *upstreamHandle, req pingRequest) return nil } -func (c *Controller) handleHeartbeatMsg(handle *upstreamHandle, hb proto.InventoryHeartbeat) error { - // XXX: when adding new services to the heartbeat logic, make sure to also update the - // 'icsServiceToMetricName' mapping in auth/grpcserver.go in order to ensure that metrics - // start counting the control stream as a registered keepalive stream for that service. - - if hb.SSHServer != nil { - if err := c.handleSSHServerHB(handle, hb.SSHServer); err != nil { - return trace.Wrap(err) - } - } - - if hb.AppServer != nil { - if err := c.handleAppServerHB(handle, hb.AppServer); err != nil { - return trace.Wrap(err) - } - } - - if hb.DatabaseServer != nil { - if err := c.handleDatabaseServerHB(handle, hb.DatabaseServer); err != nil { - return trace.Wrap(err) - } - } - - if hb.KubernetesServer != nil { - if err := c.handleKubernetesServerHB(handle, hb.KubernetesServer); err != nil { - return trace.Wrap(err) - } - } - - return nil -} - -func (c *Controller) handleSSHServerHB(handle *upstreamHandle, sshServer *types.ServerV2) error { +func (c *Controller) handleSSHServerHB(handle *upstreamHandle, sshServer *types.ServerV2, sshDelay *delay.Delay) error { // the auth layer verifies that a stream's hello message matches the identity and capabilities of the // client cert. after that point it is our responsibility to ensure that heartbeated information is // consistent with the identity and capabilities claimed in the initial hello. @@ -722,31 +773,48 @@ func (c *Controller) handleSSHServerHB(handle *upstreamHandle, sshServer *types. sshServer.SetAddr(utils.ReplaceLocalhost(sshServer.GetAddr(), handle.PeerAddr())) } + sshServer.SetExpiry(time.Now().Add(c.serverTTL).UTC()) + if handle.sshServer == nil { c.onConnectFunc(constants.KeepAliveNode) - handle.sshServer = &heartBeatInfo[*types.ServerV2]{} + if c.sshHBVariableDuration != nil { + c.sshHBVariableDuration.Inc() + } + handle.sshServer = &heartBeatInfo[*types.ServerV2]{ + resource: sshServer, + } + } else if handle.sshServer.keepAliveErrs == 0 && services.CompareServers(handle.sshServer.resource, sshServer) < services.Different { + // if we have successfully upserted this exact server the last time + // (except for the expiry), we don't need to upsert it again right now + return nil + } else { + handle.sshServer.resource = sshServer } - now := c.clock.Now() - - sshServer.SetExpiry(now.Add(c.serverTTL).UTC()) - - lease, err := c.auth.UpsertNode(c.closeContext, sshServer) - if err == nil { + if _, err := c.auth.UpsertNode(c.closeContext, handle.sshServer.resource); err == nil { c.testEvent(sshUpsertOk) - // store the new lease and reset retry state - handle.sshServer.lease = lease + // reset the error status + handle.sshServer.keepAliveErrs = 0 handle.sshServer.retryUpsert = false + + sshDelay.Reset() } else { c.testEvent(sshUpsertErr) - slog.WarnContext(c.closeContext, "Failed to upsert ssh server on heartbeat", + slog.WarnContext(c.closeContext, "Failed to announce SSH server", "server_id", handle.Hello().ServerID, "error", err, ) - // blank old lease if any and set retry state. next time handleKeepAlive is called - // we will attempt to upsert the server again. - handle.sshServer.lease = nil + // we use keepAliveErrs as a general upsert error count for SSH, + // retryUpsert as a flag to signify that we MUST succeed the very next + // upsert: if we're here it means that we have a new resource to upsert + // and we have failed to do so once, so if we fail again we are going to + // fall too far behind and we should let the instance go and connect to + // a healthier auth server + handle.sshServer.keepAliveErrs++ + if handle.sshServer.retryUpsert || handle.sshServer.keepAliveErrs > c.maxKeepAliveErrs { + return trace.Wrap(err, "failed to announce SSH server") + } handle.sshServer.retryUpsert = true } handle.sshServer.resource = sshServer @@ -772,6 +840,9 @@ func (c *Controller) handleAppServerHB(handle *upstreamHandle, appServer *types. if _, ok := handle.appServers[appKey]; !ok { c.onConnectFunc(constants.KeepAliveApp) + if c.appHBVariableDuration != nil { + c.appHBVariableDuration.Inc() + } handle.appServers[appKey] = &heartBeatInfo[*types.AppServerV3]{} } @@ -823,6 +894,9 @@ func (c *Controller) handleDatabaseServerHB(handle *upstreamHandle, databaseServ if _, ok := handle.databaseServers[dbKey]; !ok { c.onConnectFunc(constants.KeepAliveDatabase) + if c.dbHBVariableDuration != nil { + c.dbHBVariableDuration.Inc() + } handle.databaseServers[dbKey] = &heartBeatInfo[*types.DatabaseServerV3]{} } @@ -874,6 +948,9 @@ func (c *Controller) handleKubernetesServerHB(handle *upstreamHandle, kubernetes if _, ok := handle.kubernetesServers[kubeKey]; !ok { c.onConnectFunc(constants.KeepAliveKube) + if c.kubeHBVariableDuration != nil { + c.kubeHBVariableDuration.Inc() + } handle.kubernetesServers[kubeKey] = &heartBeatInfo[*types.KubernetesServerV3]{} } @@ -951,6 +1028,9 @@ func (c *Controller) keepAliveAppServer(handle *upstreamHandle, now time.Time) e if shouldRemove { c.testEvent(appKeepAliveDel) c.onDisconnectFunc(constants.KeepAliveApp, 1) + if c.appHBVariableDuration != nil { + c.appHBVariableDuration.Dec() + } delete(handle.appServers, name) } } else { @@ -1002,6 +1082,9 @@ func (c *Controller) keepAliveDatabaseServer(handle *upstreamHandle, now time.Ti if shouldRemove { c.testEvent(dbKeepAliveDel) c.onDisconnectFunc(constants.KeepAliveDatabase, 1) + if c.dbHBVariableDuration != nil { + c.dbHBVariableDuration.Dec() + } delete(handle.databaseServers, name) } } else { @@ -1053,6 +1136,9 @@ func (c *Controller) keepAliveKubernetesServer(handle *upstreamHandle, now time. if shouldRemove { c.testEvent(kubeKeepAliveDel) c.onDisconnectFunc(constants.KeepAliveKube, 1) + if c.kubeHBVariableDuration != nil { + c.kubeHBVariableDuration.Dec() + } delete(handle.kubernetesServers, name) } } else { @@ -1088,50 +1174,54 @@ func (c *Controller) keepAliveSSHServer(handle *upstreamHandle, now time.Time) e return nil } - if handle.sshServer.lease != nil { - lease := *handle.sshServer.lease - lease.Expires = now.Add(c.serverTTL).UTC() - if err := c.auth.KeepAliveServer(c.closeContext, lease); err != nil { - c.testEvent(sshKeepAliveErr) - handle.sshServer.keepAliveErrs++ - shouldClose := handle.sshServer.keepAliveErrs > c.maxKeepAliveErrs - - slog.WarnContext(c.closeContext, "Failed to keep alive ssh server", - "server_id", handle.Hello().ServerID, - "error", err, - "error_count", handle.sshServer.keepAliveErrs, - "should_remove", shouldClose, - ) - - if shouldClose { - return trace.Errorf("failed to keep alive ssh server: %v", err) - } + if _, err := c.auth.UpsertNode(c.closeContext, handle.sshServer.resource); err == nil { + if handle.sshServer.retryUpsert { + c.testEvent(sshUpsertRetryOk) } else { - handle.sshServer.keepAliveErrs = 0 c.testEvent(sshKeepAliveOk) } - } else if handle.sshServer.retryUpsert { - handle.sshServer.resource.SetExpiry(c.clock.Now().Add(c.serverTTL).UTC()) - lease, err := c.auth.UpsertNode(c.closeContext, handle.sshServer.resource) - if err != nil { + handle.sshServer.keepAliveErrs = 0 + handle.sshServer.retryUpsert = false + } else { + if handle.sshServer.retryUpsert { c.testEvent(sshUpsertRetryErr) - slog.WarnContext(c.closeContext, "Failed to upsert ssh server on retry", + slog.WarnContext(c.closeContext, "Failed to upsert SSH server on retry", "server_id", handle.Hello().ServerID, "error", err, ) - // since this is retry-specific logic, an error here means that upsert failed twice in - // a row. Missing upserts is more problematic than missing keepalives so we don'resource bother - // attempting a third time. - return trace.Errorf("failed to upsert ssh server on retry: %v", err) + // retryUpsert is set when we get a new resource and we fail to + // upsert it; if we're here it means that we have failed to upsert + // it _again_, so we have fallen quite far behind + return trace.Wrap(err, "failed to upsert SSH server on retry") + } + + c.testEvent(sshKeepAliveErr) + handle.sshServer.keepAliveErrs++ + closing := handle.sshServer.keepAliveErrs > c.maxKeepAliveErrs + slog.WarnContext(c.closeContext, "Failed to upsert SSH server on keepalive", + "server_id", handle.Hello().ServerID, + "error", err, + "count", handle.sshServer.keepAliveErrs, + "closing", closing, + ) + + if closing { + return trace.Wrap(err, "failed to keep alive SSH server") } - c.testEvent(sshUpsertRetryOk) - handle.sshServer.lease = lease - handle.sshServer.retryUpsert = false } return nil } +func (c *Controller) createKeepAliveDelay(variableDuration *interval.VariableDuration) *delay.Delay { + return delay.New(delay.Params{ + FirstInterval: retryutils.HalfJitter(c.serverKeepAlive), + FixedInterval: c.serverKeepAlive, + VariableInterval: variableDuration, + Jitter: retryutils.SeventhJitter, + }) +} + // Close terminates all control streams registered with this controller. Control streams // registered after Close() is called are closed immediately. func (c *Controller) Close() error { diff --git a/lib/inventory/controller_test.go b/lib/inventory/controller_test.go index 9ec509f725293..badc1e6920d97 100644 --- a/lib/inventory/controller_test.go +++ b/lib/inventory/controller_test.go @@ -247,14 +247,13 @@ func TestSSHServerBasics(t *testing.T) { // set up to induce some failures, but not enough to cause the control // stream to be closed. auth.mu.Lock() - auth.failUpserts = 1 - auth.failKeepAlives = 2 + auth.failUpserts = 2 auth.mu.Unlock() // keepalive should fail twice, but since the upsert is already known // to have succeeded, we should not see an upsert failure yet. awaitEvents(t, events, - expect(sshKeepAliveErr, sshKeepAliveErr), + expect(sshKeepAliveErr, sshKeepAliveErr, sshKeepAliveOk), deny(sshUpsertErr, handlerClose), ) @@ -270,6 +269,32 @@ func TestSSHServerBasics(t *testing.T) { }) require.NoError(t, err) + // this explicit upsert will not happen since the server is the same, but + // keepalives should work + awaitEvents(t, events, + expect(sshKeepAliveOk), + deny(sshKeepAliveErr, sshUpsertErr, sshUpsertRetryOk, handlerClose), + ) + + err = downstream.Send(ctx, proto.InventoryHeartbeat{ + SSHServer: &types.ServerV2{ + Metadata: types.Metadata{ + Name: serverID, + Labels: map[string]string{ + "changed": "changed", + }, + }, + Spec: types.ServerSpecV2{ + Addr: zeroAddr, + }, + }, + }) + require.NoError(t, err) + + auth.mu.Lock() + auth.failUpserts = 1 + auth.mu.Unlock() + // we should now see an upsert failure, but no additional // keepalive failures, and the upsert should succeed on retry. awaitEvents(t, events, diff --git a/lib/inventory/internal/delay/delay.go b/lib/inventory/internal/delay/delay.go index 7a5ac8a06d74a..bb94478daf875 100644 --- a/lib/inventory/internal/delay/delay.go +++ b/lib/inventory/internal/delay/delay.go @@ -74,7 +74,7 @@ type Delay struct { } // Elapsed returns the channel on which the ticks are delivered. This method can -// be called on a nil delay, resulting in a nil channel. The [*Delay.Advance] +// be called on a nil delay, resulting in a nil channel. The [Delay.Advance] // method must be called after receiving a tick from the channel. // // select { @@ -102,7 +102,7 @@ func (i *Delay) interval() time.Duration { } // Advance sets up the next tick of the delay. Must be called after receiving -// from the [*Delay.Elapsed] channel; specifically, to maintain compatibility +// from the [Delay.Elapsed] channel; specifically, to maintain compatibility // with [clockwork.Clock], it must only be called with a drained timer channel. // For consistency, the value passed to Advance should be the value received // from the Elapsed channel (passing the current time will also work, but will @@ -111,8 +111,20 @@ func (i *Delay) Advance(now time.Time) { i.timer.Reset(i.interval() - i.clock.Since(now)) } -// Stop stops the delay. Only needed for [clockwork.Clock] compatibility. Can be -// called on a nil delay, as a no-op. The delay should not be used afterwards. +// Reset restarts the ticker from the current time. Must only be called while +// the timer is running (i.e. it must not be called between receiving from +// [Delay.Elapsed] and calling [Delay.Advance]). +func (i *Delay) Reset() { + // the drain is for Go earlier than 1.23 and for [clockwork.Clock] + if !i.timer.Stop() { + <-i.timer.Chan() + } + i.timer.Reset(i.interval()) +} + +// Stop stops the delay. Only needed for Go 1.22 and [clockwork.Clock] +// compatibility. Can be called on a nil delay, as a no-op. The delay should not +// be used afterwards. func (i *Delay) Stop() { if i == nil { return From 267b9f0f2690bdb89b7a5ea4d2a6c9cf91115705 Mon Sep 17 00:00:00 2001 From: rosstimothy <39066650+rosstimothy@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:46:08 -0500 Subject: [PATCH 13/64] Convert lib/teleterm to use slog (#50335) --- lib/client/clientcache/clientcache.go | 19 ++++++------- lib/client/db/dbcmd/dbcmd.go | 24 ++++++++++------- lib/teleterm/apiserver/apiserver.go | 9 ++++--- lib/teleterm/apiserver/config.go | 11 ++++---- lib/teleterm/apiserver/middleware.go | 6 ++--- lib/teleterm/clusters/cluster.go | 10 +++---- lib/teleterm/clusters/cluster_auth.go | 14 +++++----- lib/teleterm/clusters/cluster_auth_test.go | 11 +++----- lib/teleterm/clusters/cluster_gateways.go | 6 ++--- lib/teleterm/clusters/config.go | 11 ++++---- lib/teleterm/clusters/storage.go | 12 ++++----- lib/teleterm/cmd/db_test.go | 4 +-- lib/teleterm/daemon/config.go | 12 ++++----- lib/teleterm/daemon/daemon.go | 18 ++++++------- lib/teleterm/daemon/daemon_headless.go | 27 ++++++++++++------- lib/teleterm/daemon/daemon_test.go | 4 +-- lib/teleterm/gateway/app.go | 2 +- lib/teleterm/gateway/app_middleware.go | 6 ++--- lib/teleterm/gateway/base.go | 10 +++---- lib/teleterm/gateway/config.go | 18 ++++++------- lib/teleterm/gateway/db.go | 2 +- lib/teleterm/gateway/db_middleware.go | 6 ++--- lib/teleterm/gateway/db_middleware_test.go | 4 +-- lib/teleterm/gateway/interfaces.go | 5 ++-- lib/teleterm/grpccredentials.go | 10 +++---- .../connectmycomputer/connectmycomputer.go | 25 +++++++++-------- lib/teleterm/teleterm.go | 6 ++--- tool/tsh/common/db.go | 4 +-- tool/tsh/common/proxy.go | 2 +- 29 files changed, 158 insertions(+), 140 deletions(-) diff --git a/lib/client/clientcache/clientcache.go b/lib/client/clientcache/clientcache.go index 5a9c4df29e7de..f5e8f44aafdf9 100644 --- a/lib/client/clientcache/clientcache.go +++ b/lib/client/clientcache/clientcache.go @@ -18,11 +18,11 @@ package clientcache import ( "context" + "log/slog" "slices" "sync" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "golang.org/x/sync/singleflight" "github.com/gravitational/teleport" @@ -53,7 +53,7 @@ type RetryWithReloginFunc func(ctx context.Context, tc *client.TeleportClient, f type Config struct { NewClientFunc NewClientFunc RetryWithReloginFunc RetryWithReloginFunc - Log logrus.FieldLogger + Logger *slog.Logger } func (c *Config) checkAndSetDefaults() error { @@ -63,8 +63,8 @@ func (c *Config) checkAndSetDefaults() error { if c.RetryWithReloginFunc == nil { return trace.BadParameter("RetryWithReloginFunc is required") } - if c.Log == nil { - c.Log = logrus.WithField(teleport.ComponentKey, "clientcache") + if c.Logger == nil { + c.Logger = slog.With(teleport.ComponentKey, "clientcache") } return nil } @@ -99,7 +99,7 @@ func (c *Cache) Get(ctx context.Context, profileName, leafClusterName string) (* k := key{profile: profileName, leafCluster: leafClusterName} groupClt, err, _ := c.group.Do(k.String(), func() (any, error) { if fromCache := c.getFromCache(k); fromCache != nil { - c.cfg.Log.WithField("cluster", k).Debug("Retrieved client from cache.") + c.cfg.Logger.DebugContext(ctx, "Retrieved client from cache", "cluster", k) return fromCache, nil } @@ -123,7 +123,7 @@ func (c *Cache) Get(ctx context.Context, profileName, leafClusterName string) (* // Save the client in the cache, so we don't have to build a new connection next time. c.addToCache(k, newClient) - c.cfg.Log.WithField("cluster", k).Info("Added client to cache.") + c.cfg.Logger.InfoContext(ctx, "Added client to cache", "cluster", k) return newClient, nil }) @@ -159,9 +159,10 @@ func (c *Cache) ClearForRoot(profileName string) error { } } - c.cfg.Log.WithFields( - logrus.Fields{"cluster": profileName, "clients": deleted}, - ).Info("Invalidated cached clients for root cluster.") + c.cfg.Logger.InfoContext(context.Background(), "Invalidated cached clients for root cluster", + "cluster", profileName, + "clients", deleted, + ) return trace.NewAggregate(errors...) diff --git a/lib/client/db/dbcmd/dbcmd.go b/lib/client/db/dbcmd/dbcmd.go index 603a284ea7ec9..376435b260d40 100644 --- a/lib/client/db/dbcmd/dbcmd.go +++ b/lib/client/db/dbcmd/dbcmd.go @@ -21,6 +21,7 @@ package dbcmd import ( "context" "fmt" + "log/slog" "net/url" "os" "os/exec" @@ -30,7 +31,6 @@ import ( "strings" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "go.mongodb.org/mongo-driver/x/mongo/driver/connstring" "github.com/gravitational/teleport/api/constants" @@ -143,8 +143,8 @@ func NewCmdBuilder(tc *client.TeleportClient, profile *client.ProfileStatus, host, port = tc.DatabaseProxyHostPort(db) } - if options.log == nil { - options.log = logrus.NewEntry(logrus.StandardLogger()) + if options.logger == nil { + options.logger = slog.Default() } if options.exe == nil { @@ -256,8 +256,11 @@ func (c *CLICommandBuilder) getPostgresCommand() *exec.Cmd { func (c *CLICommandBuilder) getCockroachCommand() *exec.Cmd { // If cockroach CLI client is not available, fallback to psql. if _, err := c.options.exe.LookPath(cockroachBin); err != nil { - c.options.log.Debugf("Couldn't find %q client in PATH, falling back to %q: %v.", - cockroachBin, postgresBin, err) + c.options.logger.DebugContext(context.Background(), "Couldn't find cockroach client in PATH, falling back to postgres client", + "cockroach_client", cockroachBin, + "postgres_client", postgresBin, + "error", err, + ) return c.getPostgresCommand() } return exec.Command(cockroachBin, "sql", "--url", c.getPostgresConnString()) @@ -560,7 +563,10 @@ func (c *CLICommandBuilder) getMongoAddress() string { // force a different timeout for debugging purpose or extreme situations. serverSelectionTimeoutMS := "5000" if envValue := os.Getenv(envVarMongoServerSelectionTimeoutMS); envValue != "" { - c.options.log.Infof("Using environment variable %s=%s.", envVarMongoServerSelectionTimeoutMS, envValue) + c.options.logger.InfoContext(context.Background(), "Using server selection timeout value from environment variable", + "environment_variable", envVarMongoServerSelectionTimeoutMS, + "server_selection_timeout", envValue, + ) serverSelectionTimeoutMS = envValue } query.Set("serverSelectionTimeoutMS", serverSelectionTimeoutMS) @@ -905,7 +911,7 @@ type connectionCommandOpts struct { noTLS bool printFormat bool tolerateMissingCLIClient bool - log *logrus.Entry + logger *slog.Logger exe Execer password string gcp types.GCPCloudSQL @@ -969,9 +975,9 @@ func WithPrintFormat() ConnectCommandFunc { // WithLogger is the connect command option that allows the caller to pass a logger that will be // used by CLICommandBuilder. -func WithLogger(log *logrus.Entry) ConnectCommandFunc { +func WithLogger(log *slog.Logger) ConnectCommandFunc { return func(opts *connectionCommandOpts) { - opts.log = log + opts.logger = log } } diff --git a/lib/teleterm/apiserver/apiserver.go b/lib/teleterm/apiserver/apiserver.go index f622fe5614437..42916c94b571a 100644 --- a/lib/teleterm/apiserver/apiserver.go +++ b/lib/teleterm/apiserver/apiserver.go @@ -19,11 +19,12 @@ package apiserver import ( + "context" "fmt" + "log/slog" "net" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "google.golang.org/grpc" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" @@ -70,7 +71,7 @@ func New(cfg Config) (*APIServer, error) { } grpcServer := grpc.NewServer(cfg.TshdServerCreds, - grpc.ChainUnaryInterceptor(withErrorHandling(cfg.Log)), + grpc.ChainUnaryInterceptor(withErrorHandling(cfg.Logger)), grpc.MaxConcurrentStreams(defaults.GRPCMaxConcurrentStreams), ) @@ -96,7 +97,7 @@ func (s *APIServer) Stop() { // immediate. Closing the VNet service before the gRPC server gives some time for the VNet admin // process to notice that the client is gone and shut down as well. if err := s.vnetService.Close(); err != nil { - log.WithError(err).Error("Error while closing VNet service") + slog.ErrorContext(context.Background(), "Error while closing VNet service", "error", err) } s.grpcServer.GracefulStop() @@ -120,7 +121,7 @@ func newListener(hostAddr string, listeningC chan<- utils.NetAddr) (net.Listener listeningC <- addr } - log.Infof("tsh daemon is listening on %v.", addr.FullAddress()) + slog.InfoContext(context.Background(), "tsh daemon listener created", "listen_addr", addr.FullAddress()) return lis, nil } diff --git a/lib/teleterm/apiserver/config.go b/lib/teleterm/apiserver/config.go index 76495b8a181b2..37e233a33f706 100644 --- a/lib/teleterm/apiserver/config.go +++ b/lib/teleterm/apiserver/config.go @@ -19,9 +19,10 @@ package apiserver import ( + "log/slog" + "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "google.golang.org/grpc" "github.com/gravitational/teleport" @@ -39,8 +40,8 @@ type Config struct { Daemon *daemon.Service ClusterIDCache *clusteridcache.Cache InstallationID string - // Log is a component logger - Log logrus.FieldLogger + // Logger is a component logger + Logger *slog.Logger TshdServerCreds grpc.ServerOption Clock clockwork.Clock // ListeningC propagates the address on which the gRPC server listens. Mostly useful in tests, as @@ -66,8 +67,8 @@ func (c *Config) CheckAndSetDefaults() error { return trace.BadParameter("missing TshdServerCreds") } - if c.Log == nil { - c.Log = logrus.WithField(teleport.ComponentKey, "conn:apiserver") + if c.Logger == nil { + c.Logger = slog.With(teleport.ComponentKey, "conn:apiserver") } if c.InstallationID == "" { diff --git a/lib/teleterm/apiserver/middleware.go b/lib/teleterm/apiserver/middleware.go index 7f8bb787a4038..520b97bb76565 100644 --- a/lib/teleterm/apiserver/middleware.go +++ b/lib/teleterm/apiserver/middleware.go @@ -20,14 +20,14 @@ package apiserver import ( "context" + "log/slog" "github.com/gravitational/trace/trail" - "github.com/sirupsen/logrus" "google.golang.org/grpc" ) // withErrorHandling is gRPC middleware that maps internal errors to proper gRPC error codes -func withErrorHandling(log logrus.FieldLogger) grpc.UnaryServerInterceptor { +func withErrorHandling(log *slog.Logger) grpc.UnaryServerInterceptor { return func( ctx context.Context, req interface{}, @@ -36,7 +36,7 @@ func withErrorHandling(log logrus.FieldLogger) grpc.UnaryServerInterceptor { ) (interface{}, error) { resp, err := handler(ctx, req) if err != nil { - log.WithError(err).Error("Request failed.") + log.ErrorContext(ctx, "Request failed", "error", err) return resp, trail.ToGRPC(err) } diff --git a/lib/teleterm/clusters/cluster.go b/lib/teleterm/clusters/cluster.go index 3899dc64fadff..ef8f22e461c86 100644 --- a/lib/teleterm/clusters/cluster.go +++ b/lib/teleterm/clusters/cluster.go @@ -20,10 +20,10 @@ package clusters import ( "context" + "log/slog" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" "google.golang.org/grpc" "google.golang.org/grpc/metadata" @@ -49,8 +49,8 @@ type Cluster struct { Name string // ProfileName is the name of the tsh profile ProfileName string - // Log is a component logger - Log *logrus.Entry + // Logger is a component logger + Logger *slog.Logger // dir is the directory where cluster certificates are stored dir string // Status is the cluster status @@ -192,9 +192,7 @@ func (c *Cluster) GetWithDetails(ctx context.Context, authClient authclient.Clie return roles, nil }) if err != nil { - c.Log. - WithError(err). - Warn("Failed to calculate trusted device requirement") + c.Logger.WarnContext(ctx, "Failed to calculate trusted device requirement", "error", err) } roleSet := services.NewRoleSet(roles...) diff --git a/lib/teleterm/clusters/cluster_auth.go b/lib/teleterm/clusters/cluster_auth.go index c8b8b4ebe1a40..e50256410ee48 100644 --- a/lib/teleterm/clusters/cluster_auth.go +++ b/lib/teleterm/clusters/cluster_auth.go @@ -22,10 +22,10 @@ import ( "context" "encoding/json" "errors" + "log/slog" "sort" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport/api/client/webclient" "github.com/gravitational/teleport/api/constants" @@ -47,9 +47,9 @@ func (c *Cluster) SyncAuthPreference(ctx context.Context) (*webclient.WebConfigA } pingResponseJSON, err := json.Marshal(pingResponse) if err != nil { - c.Log.WithError(err).Debugln("Could not marshal ping response to JSON") + c.Logger.DebugContext(ctx, "Could not marshal ping response to JSON", "error", err) } else { - c.Log.WithField("response", string(pingResponseJSON)).Debugln("Got ping response") + c.Logger.DebugContext(ctx, "Got ping response", "response", string(pingResponseJSON)) } if err := c.clusterClient.SaveProfile(false); err != nil { @@ -227,7 +227,7 @@ func (c *Cluster) passwordlessLogin(stream api.TerminalService_LoginPasswordless response, err := client.SSHAgentPasswordlessLogin(ctx, client.SSHLoginPasswordless{ SSHLogin: sshLogin, AuthenticatorAttachment: c.clusterClient.AuthenticatorAttachment, - CustomPrompt: newPwdlessLoginPrompt(ctx, c.Log, stream), + CustomPrompt: newPwdlessLoginPrompt(ctx, c.Logger, stream), WebauthnLogin: c.clusterClient.WebauthnLogin, }) if err != nil { @@ -239,11 +239,11 @@ func (c *Cluster) passwordlessLogin(stream api.TerminalService_LoginPasswordless // pwdlessLoginPrompt is a implementation for wancli.LoginPrompt for teleterm passwordless logins. type pwdlessLoginPrompt struct { - log *logrus.Entry + log *slog.Logger Stream api.TerminalService_LoginPasswordlessServer } -func newPwdlessLoginPrompt(ctx context.Context, log *logrus.Entry, stream api.TerminalService_LoginPasswordlessServer) *pwdlessLoginPrompt { +func newPwdlessLoginPrompt(ctx context.Context, log *slog.Logger, stream api.TerminalService_LoginPasswordlessServer) *pwdlessLoginPrompt { return &pwdlessLoginPrompt{ log: log, Stream: stream, @@ -283,7 +283,7 @@ func (p *pwdlessLoginPrompt) ackTouch() error { // The current gRPC message type switch in teleterm client code will reject // any new message types, making this difficult to add without breaking // older clients. - p.log.Debug("Detected security key tap") + p.log.DebugContext(context.Background(), "Detected security key tap") return nil } diff --git a/lib/teleterm/clusters/cluster_auth_test.go b/lib/teleterm/clusters/cluster_auth_test.go index f9c7cd8e2c4ea..36165f053c1eb 100644 --- a/lib/teleterm/clusters/cluster_auth_test.go +++ b/lib/teleterm/clusters/cluster_auth_test.go @@ -20,20 +20,17 @@ package clusters import ( "context" + "log/slog" "testing" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "google.golang.org/grpc" - "github.com/gravitational/teleport" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" wancli "github.com/gravitational/teleport/lib/auth/webauthncli" ) -var log = logrus.WithField(teleport.ComponentKey, "cluster_auth_test") - func TestPwdlessLoginPrompt_PromptPIN(t *testing.T) { stream := &mockLoginPwdlessStream{} @@ -49,7 +46,7 @@ func TestPwdlessLoginPrompt_PromptPIN(t *testing.T) { }}, nil } - prompt := newPwdlessLoginPrompt(context.Background(), log, stream) + prompt := newPwdlessLoginPrompt(context.Background(), slog.Default(), stream) pin, err := prompt.PromptPIN() require.NoError(t, err) require.Equal(t, "1234", pin) @@ -74,7 +71,7 @@ func TestPwdlessLoginPrompt_PromptTouch(t *testing.T) { return nil } - prompt := newPwdlessLoginPrompt(context.Background(), log, stream) + prompt := newPwdlessLoginPrompt(context.Background(), slog.Default(), stream) ackTouch, err := prompt.PromptTouch() require.NoError(t, err) require.NoError(t, ackTouch()) @@ -110,7 +107,7 @@ func TestPwdlessLoginPrompt_PromptCredential(t *testing.T) { }}, nil } - prompt := newPwdlessLoginPrompt(context.Background(), log, stream) + prompt := newPwdlessLoginPrompt(context.Background(), slog.Default(), stream) cred, err := prompt.PromptCredential(unsortedCreds) require.NoError(t, err) require.Equal(t, "foo", cred.User.Name) diff --git a/lib/teleterm/clusters/cluster_gateways.go b/lib/teleterm/clusters/cluster_gateways.go index 590fa27611f21..64577c35cf7dd 100644 --- a/lib/teleterm/clusters/cluster_gateways.go +++ b/lib/teleterm/clusters/cluster_gateways.go @@ -105,7 +105,7 @@ func (c *Cluster) createDBGateway(ctx context.Context, params CreateGatewayParam Cert: cert, Insecure: c.clusterClient.InsecureSkipVerify, WebProxyAddr: c.clusterClient.WebProxyAddr, - Log: c.Log, + Logger: c.Logger, TCPPortAllocator: params.TCPPortAllocator, OnExpiredCert: params.OnExpiredCert, Clock: c.clock, @@ -145,7 +145,7 @@ func (c *Cluster) createKubeGateway(ctx context.Context, params CreateGatewayPar Cert: cert, Insecure: c.clusterClient.InsecureSkipVerify, WebProxyAddr: c.clusterClient.WebProxyAddr, - Log: c.Log, + Logger: c.Logger, TCPPortAllocator: params.TCPPortAllocator, OnExpiredCert: params.OnExpiredCert, Clock: c.clock, @@ -187,7 +187,7 @@ func (c *Cluster) createAppGateway(ctx context.Context, params CreateGatewayPara Protocol: app.GetProtocol(), Insecure: c.clusterClient.InsecureSkipVerify, WebProxyAddr: c.clusterClient.WebProxyAddr, - Log: c.Log, + Logger: c.Logger, TCPPortAllocator: params.TCPPortAllocator, OnExpiredCert: params.OnExpiredCert, Clock: c.clock, diff --git a/lib/teleterm/clusters/config.go b/lib/teleterm/clusters/config.go index 6af0ad1bbfad3..ff94f4fdb533a 100644 --- a/lib/teleterm/clusters/config.go +++ b/lib/teleterm/clusters/config.go @@ -19,9 +19,10 @@ package clusters import ( + "log/slog" + "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/utils/keys" @@ -37,8 +38,8 @@ type Config struct { Clock clockwork.Clock // InsecureSkipVerify is an option to skip TLS cert check InsecureSkipVerify bool - // Log is a component logger - Log *logrus.Entry + // Logger is a component logger + Logger *slog.Logger // WebauthnLogin allows tests to override the Webauthn Login func. // Defaults to wancli.Login. WebauthnLogin client.WebauthnLoginFunc @@ -63,8 +64,8 @@ func (c *Config) CheckAndSetDefaults() error { c.Clock = clockwork.NewRealClock() } - if c.Log == nil { - c.Log = logrus.WithField(teleport.ComponentKey, "conn:storage") + if c.Logger == nil { + c.Logger = slog.With(teleport.ComponentKey, "conn:storage") } if c.AddKeysToAgent == "" { diff --git a/lib/teleterm/clusters/storage.go b/lib/teleterm/clusters/storage.go index f00adfc73c15c..7d5b1292aa25e 100644 --- a/lib/teleterm/clusters/storage.go +++ b/lib/teleterm/clusters/storage.go @@ -179,13 +179,13 @@ func (s *Storage) addCluster(ctx context.Context, dir, webProxyAddress string) ( return nil, nil, trace.Wrap(err) } - clusterLog := s.Log.WithField("cluster", clusterURI) + clusterLog := s.Logger.With("cluster", clusterURI) pingResponseJSON, err := json.Marshal(pingResponse) if err != nil { - clusterLog.WithError(err).Debugln("Could not marshal ping response to JSON") + clusterLog.DebugContext(ctx, "Could not marshal ping response to JSON", "error", err) } else { - clusterLog.WithField("response", string(pingResponseJSON)).Debugln("Got ping response") + clusterLog.DebugContext(ctx, "Got ping response", "response", string(pingResponseJSON)) } if err := clusterClient.SaveProfile(false); err != nil { @@ -201,7 +201,7 @@ func (s *Storage) addCluster(ctx context.Context, dir, webProxyAddress string) ( clusterClient: clusterClient, dir: s.Dir, clock: s.Clock, - Log: clusterLog, + Logger: clusterLog, }, clusterClient, nil } @@ -241,7 +241,7 @@ func (s *Storage) fromProfile(profileName, leafClusterName string) (*Cluster, *c dir: s.Dir, clock: s.Clock, statusError: err, - Log: s.Log.WithField("cluster", clusterURI), + Logger: s.Logger.With("cluster", clusterURI), } if status != nil { cluster.status = *status @@ -258,7 +258,7 @@ func (s *Storage) loadProfileStatusAndClusterKey(clusterClient *client.TeleportC _, err := clusterClient.LocalAgent().GetKeyRing(clusterNameForKey) if err != nil { if trace.IsNotFound(err) { - s.Log.Infof("No keys found for cluster %v.", clusterNameForKey) + s.Logger.InfoContext(context.Background(), "No keys found for cluster", "cluster", clusterNameForKey) } else { return nil, trace.Wrap(err) } diff --git a/lib/teleterm/cmd/db_test.go b/lib/teleterm/cmd/db_test.go index cd165b850cdc4..68f7a82fdcec4 100644 --- a/lib/teleterm/cmd/db_test.go +++ b/lib/teleterm/cmd/db_test.go @@ -21,12 +21,12 @@ package cmd import ( "context" "fmt" + "log/slog" "os/exec" "path/filepath" "testing" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/client/proto" @@ -66,7 +66,7 @@ func (m fakeDatabaseGateway) TargetName() string { return m.targetURI func (m fakeDatabaseGateway) TargetUser() string { return "alice" } func (m fakeDatabaseGateway) TargetSubresourceName() string { return m.subresourceName } func (m fakeDatabaseGateway) Protocol() string { return m.protocol } -func (m fakeDatabaseGateway) Log() *logrus.Entry { return nil } +func (m fakeDatabaseGateway) Log() *slog.Logger { return nil } func (m fakeDatabaseGateway) LocalAddress() string { return "localhost" } func (m fakeDatabaseGateway) LocalPortInt() int { return 8888 } func (m fakeDatabaseGateway) LocalPort() string { return "8888" } diff --git a/lib/teleterm/daemon/config.go b/lib/teleterm/daemon/config.go index 80cc79d081946..3646b78f05e2b 100644 --- a/lib/teleterm/daemon/config.go +++ b/lib/teleterm/daemon/config.go @@ -20,10 +20,10 @@ package daemon import ( "context" + "log/slog" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "google.golang.org/grpc" "github.com/gravitational/teleport" @@ -52,8 +52,8 @@ type Config struct { Clock clockwork.Clock // Storage is a storage service that reads/writes to tsh profiles Storage Storage - // Log is a component logger - Log *logrus.Entry + // Logger is a component logger + Logger *slog.Logger // PrehogAddr is the URL where prehog events should be submitted. PrehogAddr string // KubeconfigsDir is the directory containing kubeconfigs for Kubernetes @@ -121,8 +121,8 @@ func (c *Config) CheckAndSetDefaults() error { c.GatewayCreator = clusters.NewGatewayCreator(c.Storage) } - if c.Log == nil { - c.Log = logrus.NewEntry(logrus.StandardLogger()).WithField(teleport.ComponentKey, "daemon") + if c.Logger == nil { + c.Logger = slog.With(teleport.ComponentKey, "daemon") } if c.ConnectMyComputerRoleSetup == nil { @@ -172,7 +172,7 @@ func (c *Config) CheckAndSetDefaults() error { return clusters.AddMetadataToRetryableError(ctx, fn) } return clientcache.New(clientcache.Config{ - Log: c.Log, + Logger: c.Logger, NewClientFunc: newClientFunc, RetryWithReloginFunc: clientcache.RetryWithReloginFunc(retryWithRelogin), }) diff --git a/lib/teleterm/daemon/daemon.go b/lib/teleterm/daemon/daemon.go index 13f12f4dfa253..d3528793a4b99 100644 --- a/lib/teleterm/daemon/daemon.go +++ b/lib/teleterm/daemon/daemon.go @@ -357,7 +357,7 @@ func (s *Service) createGateway(ctx context.Context, params CreateGatewayParams) go func() { if err := gateway.Serve(); err != nil { - gateway.Log().WithError(err).Warn("Failed to handle a gateway connection.") + gateway.Log().WarnContext(ctx, "Failed to handle a gateway connection", "error", err) } }() @@ -416,7 +416,7 @@ func (s *Service) reissueGatewayCerts(ctx context.Context, g gateway.Gateway) (t }, }) if notifyErr != nil { - s.cfg.Log.WithError(notifyErr).Error("Failed to send a notification for an error encountered during gateway cert reissue") + s.cfg.Logger.ErrorContext(ctx, "Failed to send a notification for an error encountered during gateway cert reissue", "error", notifyErr) } // Return the error to the alpn.LocalProxy's middleware. @@ -559,9 +559,9 @@ func (s *Service) SetGatewayLocalPort(gatewayURI, localPort string) (gateway.Gat // 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, + newGateway.Log().WarnContext(s.closeContext, + "Failed to close the new gateway after failing to close the old gateway", + "error", newGatewayCloseErr, ) } return nil, trace.Wrap(err) @@ -571,7 +571,7 @@ func (s *Service) SetGatewayLocalPort(gatewayURI, localPort string) (gateway.Gat go func() { if err := newGateway.Serve(); err != nil { - newGateway.Log().WithError(err).Warn("Failed to handle a gateway connection.") + newGateway.Log().WarnContext(s.closeContext, "Failed to handle a gateway connection", "error", err) } }() @@ -842,7 +842,7 @@ func (s *Service) Stop() { s.mu.RLock() defer s.mu.RUnlock() - s.cfg.Log.Info("Stopping") + s.cfg.Logger.InfoContext(s.closeContext, "Stopping") for _, gateway := range s.gateways { gateway.Close() @@ -851,14 +851,14 @@ func (s *Service) Stop() { s.StopHeadlessWatchers() if err := s.clientCache.Clear(); err != nil { - s.cfg.Log.WithError(err).Error("Failed to close remote clients") + s.cfg.Logger.ErrorContext(s.closeContext, "Failed to close remote clients", "error", err) } 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.cfg.Logger.ErrorContext(timeoutCtx, "Gracefully stopping usage reporter failed", "error", err) } // s.closeContext is used for the tshd events client which might make requests as long as any of diff --git a/lib/teleterm/daemon/daemon_headless.go b/lib/teleterm/daemon/daemon_headless.go index 9ddf02a52405c..310b853d229c3 100644 --- a/lib/teleterm/daemon/daemon_headless.go +++ b/lib/teleterm/daemon/daemon_headless.go @@ -30,6 +30,7 @@ import ( api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/teleterm/clusters" + logutils "github.com/gravitational/teleport/lib/utils/log" ) // UpdateHeadlessAuthenticationState updates a headless authentication state. @@ -96,7 +97,7 @@ func (s *Service) startHeadlessWatcher(rootCluster *clusters.Cluster, waitInit b watchCtx, watchCancel := context.WithCancel(s.closeContext) s.headlessWatcherClosers[rootCluster.URI.String()] = watchCancel - log := s.cfg.Log.WithField("cluster", rootCluster.URI.String()) + log := s.cfg.Logger.With("cluster", logutils.StringerAttr(rootCluster.URI)) pendingRequests := make(map[string]context.CancelFunc) pendingRequestsMu := sync.Mutex{} @@ -180,7 +181,7 @@ func (s *Service) startHeadlessWatcher(rootCluster *clusters.Cluster, waitInit b defer cancelSend() if err := s.sendPendingHeadlessAuthentication(sendCtx, ha, rootCluster.URI.String()); err != nil { if !strings.Contains(err.Error(), context.Canceled.Error()) && !strings.Contains(err.Error(), context.DeadlineExceeded.Error()) { - log.WithError(err).Debug("sendPendingHeadlessAuthentication resulted in unexpected error.") + log.DebugContext(sendCtx, "sendPendingHeadlessAuthentication resulted in unexpected error", "error", err) } } }() @@ -210,7 +211,7 @@ func (s *Service) startHeadlessWatcher(rootCluster *clusters.Cluster, waitInit b } } - log.Debugf("Starting headless watch loop.") + log.DebugContext(watchCtx, "Starting headless watch loop") go func() { defer func() { s.headlessWatcherClosersMu.Lock() @@ -222,31 +223,36 @@ func (s *Service) startHeadlessWatcher(rootCluster *clusters.Cluster, waitInit b default: // watcher closed due to error or cluster disconnect. if err := s.stopHeadlessWatcher(rootCluster.URI.String()); err != nil { - log.WithError(err).Debug("Failed to remove headless watcher.") + log.DebugContext(watchCtx, "Failed to remove headless watcher", "error", err) } } }() for { if !rootCluster.Connected() { - log.Debugf("Not connected to cluster. Returning from headless watch loop.") + log.DebugContext(watchCtx, "Not connected to cluster, terminating headless watch loop") return } err := watch() if trace.IsNotImplemented(err) { // Don't retry watch if we are connecting to an old Auth Server. - log.WithError(err).Debug("Headless watcher not supported.") + log.DebugContext(watchCtx, "Headless watcher not supported", "error", err) return } startedWaiting := s.cfg.Clock.Now() select { case t := <-retry.After(): - log.WithError(err).Debugf("Restarting watch on error after waiting %v.", t.Sub(startedWaiting)) + log.DebugContext(watchCtx, "Restarting watch on error", + "backoff", t.Sub(startedWaiting), + "error", err, + ) retry.Inc() case <-watchCtx.Done(): - log.WithError(watchCtx.Err()).Debugf("Context closed with err. Returning from headless watch loop.") + log.DebugContext(watchCtx, "Context closed with error, ending headless watch loop", + "error", watchCtx.Err(), + ) return } } @@ -295,7 +301,10 @@ func (s *Service) StopHeadlessWatchers() { for uri := range s.headlessWatcherClosers { if err := s.stopHeadlessWatcher(uri); err != nil { - s.cfg.Log.WithField("cluster", uri).WithError(err).Debug("Encountered unexpected error closing headless watcher") + s.cfg.Logger.DebugContext(s.closeContext, "Encountered unexpected error closing headless watcher", + "error", err, + "cluster", uri, + ) } } } diff --git a/lib/teleterm/daemon/daemon_test.go b/lib/teleterm/daemon/daemon_test.go index 9c46057f7ac30..6f5670d61fe57 100644 --- a/lib/teleterm/daemon/daemon_test.go +++ b/lib/teleterm/daemon/daemon_test.go @@ -21,6 +21,7 @@ package daemon import ( "context" "errors" + "log/slog" "net" "net/http" "net/http/httptest" @@ -30,7 +31,6 @@ import ( "time" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" @@ -726,7 +726,7 @@ func (m fakeGateway) TargetName() string { return m.targetURI.GetDbNa func (m fakeGateway) TargetUser() string { return "alice" } func (m fakeGateway) TargetSubresourceName() string { return m.subresourceName } func (m fakeGateway) Protocol() string { return defaults.ProtocolSQLServer } -func (m fakeGateway) Log() *logrus.Entry { return nil } +func (m fakeGateway) Log() *slog.Logger { return nil } func (m fakeGateway) LocalAddress() string { return "localhost" } func (m fakeGateway) LocalPortInt() int { return 8888 } func (m fakeGateway) LocalPort() string { return "8888" } diff --git a/lib/teleterm/gateway/app.go b/lib/teleterm/gateway/app.go index 603d640a05a9c..110d36604aeff 100644 --- a/lib/teleterm/gateway/app.go +++ b/lib/teleterm/gateway/app.go @@ -56,7 +56,7 @@ func makeAppGateway(cfg Config) (Gateway, error) { } middleware := &appMiddleware{ - log: a.cfg.Log, + logger: a.cfg.Logger, onExpiredCert: func(ctx context.Context) (tls.Certificate, error) { cert, err := a.cfg.OnExpiredCert(ctx, a) return cert, trace.Wrap(err) diff --git a/lib/teleterm/gateway/app_middleware.go b/lib/teleterm/gateway/app_middleware.go index 8af69271ade03..9b58de8624016 100644 --- a/lib/teleterm/gateway/app_middleware.go +++ b/lib/teleterm/gateway/app_middleware.go @@ -21,16 +21,16 @@ import ( "crypto/tls" "crypto/x509" "errors" + "log/slog" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" alpn "github.com/gravitational/teleport/lib/srv/alpnproxy" ) type appMiddleware struct { onExpiredCert func(context.Context) (tls.Certificate, error) - log *logrus.Entry + logger *slog.Logger } // OnNewConnection calls m.onExpiredCert to get a fresh cert if the cert has expired and then sets @@ -48,7 +48,7 @@ func (m *appMiddleware) OnNewConnection(ctx context.Context, lp *alpn.LocalProxy return trace.Wrap(err) } - m.log.WithError(err).Debug("Gateway certificates have expired") + m.logger.DebugContext(ctx, "Gateway certificates have expired", "error", err) cert, err := m.onExpiredCert(ctx) if err != nil { diff --git a/lib/teleterm/gateway/base.go b/lib/teleterm/gateway/base.go index e0a9a33cc4d86..3a8b076307c60 100644 --- a/lib/teleterm/gateway/base.go +++ b/lib/teleterm/gateway/base.go @@ -21,11 +21,11 @@ package gateway import ( "context" "fmt" + "log/slog" "net" "strconv" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" alpn "github.com/gravitational/teleport/lib/srv/alpnproxy" "github.com/gravitational/teleport/lib/teleterm/api/uri" @@ -107,8 +107,8 @@ func (b *base) Close() error { // Serve starts the underlying ALPN proxy. Blocks until closeContext is canceled. func (b *base) Serve() error { - b.cfg.Log.Info("Gateway is open.") - defer b.cfg.Log.Info("Gateway has closed.") + b.cfg.Logger.InfoContext(b.closeContext, "Gateway is open") + defer b.cfg.Logger.InfoContext(b.closeContext, "Gateway has closed") if b.forwardProxy != nil { return trace.Wrap(b.serveWithForwardProxy()) @@ -165,8 +165,8 @@ func (b *base) SetTargetSubresourceName(value string) { b.cfg.TargetSubresourceName = value } -func (b *base) Log() *logrus.Entry { - return b.cfg.Log +func (b *base) Log() *slog.Logger { + return b.cfg.Logger } func (b *base) LocalAddress() string { diff --git a/lib/teleterm/gateway/config.go b/lib/teleterm/gateway/config.go index c870df9075728..67768d05900db 100644 --- a/lib/teleterm/gateway/config.go +++ b/lib/teleterm/gateway/config.go @@ -22,13 +22,13 @@ import ( "context" "crypto/tls" "crypto/x509" + "log/slog" "net" "runtime" "github.com/google/uuid" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/lib/defaults" @@ -69,8 +69,8 @@ type Config struct { Username string // WebProxyAddr WebProxyAddr string - // Log is a component logger - Log *logrus.Entry + // Logger is a component logger + Logger *slog.Logger // TCPPortAllocator creates listeners on the given ports. This interface lets us avoid occupying // hardcoded ports in tests. TCPPortAllocator TCPPortAllocator @@ -125,8 +125,8 @@ func (c *Config) CheckAndSetDefaults() error { c.LocalPort = "0" } - if c.Log == nil { - c.Log = logrus.NewEntry(logrus.StandardLogger()) + if c.Logger == nil { + c.Logger = slog.Default() } if c.TargetName == "" { @@ -154,10 +154,10 @@ func (c *Config) CheckAndSetDefaults() error { } } - c.Log = c.Log.WithFields(logrus.Fields{ - "resource": c.TargetURI.String(), - "gateway": c.URI.String(), - }) + c.Logger = c.Logger.With( + "resource", c.TargetURI.String(), + "gateway", c.URI.String(), + ) return nil } diff --git a/lib/teleterm/gateway/db.go b/lib/teleterm/gateway/db.go index a6b25e685b9c8..b1602d0648b08 100644 --- a/lib/teleterm/gateway/db.go +++ b/lib/teleterm/gateway/db.go @@ -54,7 +54,7 @@ func makeDatabaseGateway(cfg Config) (Database, error) { } middleware := &dbMiddleware{ - log: d.cfg.Log, + logger: d.cfg.Logger, onExpiredCert: func(ctx context.Context) (tls.Certificate, error) { cert, err := d.cfg.OnExpiredCert(ctx, d) return cert, trace.Wrap(err) diff --git a/lib/teleterm/gateway/db_middleware.go b/lib/teleterm/gateway/db_middleware.go index cd189fff048a0..110f6969d41a8 100644 --- a/lib/teleterm/gateway/db_middleware.go +++ b/lib/teleterm/gateway/db_middleware.go @@ -23,9 +23,9 @@ import ( "crypto/tls" "crypto/x509" "errors" + "log/slog" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" alpn "github.com/gravitational/teleport/lib/srv/alpnproxy" "github.com/gravitational/teleport/lib/tlsca" @@ -33,7 +33,7 @@ import ( type dbMiddleware struct { onExpiredCert func(context.Context) (tls.Certificate, error) - log *logrus.Entry + logger *slog.Logger dbRoute tlsca.RouteToDatabase } @@ -54,7 +54,7 @@ func (m *dbMiddleware) OnNewConnection(ctx context.Context, lp *alpn.LocalProxy) return trace.Wrap(err) } - m.log.WithError(err).Debug("Gateway certificates have expired") + m.logger.DebugContext(ctx, "Gateway certificates have expired", "error", err) cert, err := m.onExpiredCert(ctx) if err != nil { diff --git a/lib/teleterm/gateway/db_middleware_test.go b/lib/teleterm/gateway/db_middleware_test.go index cecf12306de2e..1f786a5d8226c 100644 --- a/lib/teleterm/gateway/db_middleware_test.go +++ b/lib/teleterm/gateway/db_middleware_test.go @@ -21,11 +21,11 @@ package gateway import ( "context" "crypto/tls" + "log/slog" "testing" "time" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/gravitational/teleport" @@ -112,7 +112,7 @@ func TestDBMiddleware_OnNewConnection(t *testing.T) { hasCalledOnExpiredCert = true return tls.Certificate{}, nil }, - log: logrus.WithField(teleport.ComponentKey, "middleware"), + logger: slog.With(teleport.ComponentKey, "middleware"), dbRoute: tt.dbRoute, } diff --git a/lib/teleterm/gateway/interfaces.go b/lib/teleterm/gateway/interfaces.go index 4cedf02e7ffd7..27bc6735a2b9d 100644 --- a/lib/teleterm/gateway/interfaces.go +++ b/lib/teleterm/gateway/interfaces.go @@ -19,8 +19,9 @@ package gateway import ( + "log/slog" + "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport/lib/teleterm/api/uri" "github.com/gravitational/teleport/lib/tlsca" @@ -41,7 +42,7 @@ type Gateway interface { TargetUser() string TargetSubresourceName() string SetTargetSubresourceName(value string) - Log() *logrus.Entry + Log() *slog.Logger LocalAddress() string LocalPort() string LocalPortInt() int diff --git a/lib/teleterm/grpccredentials.go b/lib/teleterm/grpccredentials.go index f0c7c7562927f..8228f2e7ae631 100644 --- a/lib/teleterm/grpccredentials.go +++ b/lib/teleterm/grpccredentials.go @@ -21,11 +21,11 @@ package teleterm import ( "crypto/tls" "crypto/x509" + "log/slog" "os" "path/filepath" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -65,21 +65,21 @@ func createServerCredentials(serverKeyPair tls.Certificate, clientCertPaths []st Certificates: []tls.Certificate{serverKeyPair}, } - config.GetConfigForClient = func(_ *tls.ClientHelloInfo) (*tls.Config, error) { + config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) { certPool := x509.NewCertPool() for _, clientCertPath := range clientCertPaths { - log := log.WithField("cert_path", clientCertPath) + log := slog.With("cert_path", clientCertPath) clientCert, err := os.ReadFile(clientCertPath) if err != nil { - log.WithError(err).Error("Failed to read the client cert file") + log.ErrorContext(info.Context(), "Failed to read the client cert file", "error", err) // Fall back to the default config. return nil, nil } if !certPool.AppendCertsFromPEM(clientCert) { - log.Error("Failed to add the client cert to the pool") + log.ErrorContext(info.Context(), "Failed to add the client cert to the pool") // Fall back to the default config. return nil, nil } diff --git a/lib/teleterm/services/connectmycomputer/connectmycomputer.go b/lib/teleterm/services/connectmycomputer/connectmycomputer.go index 26ecc8aafe8d9..d26e17621c9b5 100644 --- a/lib/teleterm/services/connectmycomputer/connectmycomputer.go +++ b/lib/teleterm/services/connectmycomputer/connectmycomputer.go @@ -21,6 +21,7 @@ package connectmycomputer import ( "context" "fmt" + "log/slog" "os" "os/user" "path/filepath" @@ -31,7 +32,6 @@ import ( "github.com/google/uuid" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport" apidefaults "github.com/gravitational/teleport/api/defaults" @@ -105,8 +105,9 @@ func (s *RoleSetup) Run(ctx context.Context, accessAndIdentity AccessAndIdentity reloadCerts := false + logger := s.cfg.Logger.With("role", roleName) if !doesRoleExist { - s.cfg.Log.Infof("Creating the role %v.", roleName) + logger.InfoContext(ctx, "Creating the role") role, err := types.NewRole(roleName, types.RoleSpecV6{ Allow: types.RoleConditions{ @@ -123,7 +124,7 @@ func (s *RoleSetup) Run(ctx context.Context, accessAndIdentity AccessAndIdentity return noCertsReloaded, trace.Wrap(err, "creating role %v", roleName) } } else { - s.cfg.Log.Infof("The role %v already exists", roleName) + logger.InfoContext(ctx, "The role already exists") isRoleDirty := false // Ensure that the current system username is in the role. @@ -134,7 +135,9 @@ func (s *RoleSetup) Run(ctx context.Context, accessAndIdentity AccessAndIdentity allowedLogins := existingRole.GetLogins(types.Allow) if !slices.Contains(allowedLogins, systemUser.Username) { - s.cfg.Log.Infof("Adding %v to the logins of the role %v.", systemUser.Username, roleName) + logger.InfoContext(ctx, "Adding username to the logins of the role", + "username", systemUser.Username, + ) existingRole.SetLogins(types.Allow, append(allowedLogins, systemUser.Username)) isRoleDirty = true @@ -156,7 +159,7 @@ func (s *RoleSetup) Run(ctx context.Context, accessAndIdentity AccessAndIdentity expectedOwnerNodeLabelValue := []string{clusterUser.GetName()} if !slices.Equal(ownerNodeLabelValue, expectedOwnerNodeLabelValue) { - s.cfg.Log.Infof("Overwriting the owner node label in the role %v.", roleName) + logger.InfoContext(ctx, "Overwriting the owner node label in the role") allowedNodeLabels[types.ConnectMyComputerNodeOwnerLabel] = expectedOwnerNodeLabelValue isRoleDirty = true @@ -178,9 +181,9 @@ func (s *RoleSetup) Run(ctx context.Context, accessAndIdentity AccessAndIdentity hasCMCRole := slices.Contains(clusterUser.GetRoles(), roleName) if hasCMCRole { - s.cfg.Log.Infof("The user %v already has the role %v.", clusterUser.GetName(), roleName) + logger.InfoContext(ctx, "The user already has the role", "user", clusterUser.GetName()) } else { - s.cfg.Log.Infof("Adding the role %v to the user %v.", roleName, clusterUser.GetName()) + logger.InfoContext(ctx, "Adding the role to the user", "user", clusterUser.GetName()) clusterUser.AddRole(roleName) timeoutCtx, cancel := context.WithTimeout(ctx, resourceUpdateTimeout) defer cancel() @@ -197,7 +200,7 @@ func (s *RoleSetup) Run(ctx context.Context, accessAndIdentity AccessAndIdentity } if reloadCerts { - s.cfg.Log.Info("Reissuing certs.") + s.cfg.Logger.InfoContext(ctx, "Reissuing certs") // ReissueUserCerts called with CertCacheDrop and a bogus access request ID in DropAccessRequests // allows us to refresh the role list in the certs without forcing the user to relogin. // @@ -273,12 +276,12 @@ type CertManager interface { } type RoleSetupConfig struct { - Log *logrus.Entry + Logger *slog.Logger } func (c *RoleSetupConfig) CheckAndSetDefaults() error { - if c.Log == nil { - c.Log = logrus.NewEntry(logrus.StandardLogger()).WithField(teleport.ComponentKey, "CMC role") + if c.Logger == nil { + c.Logger = slog.With(teleport.ComponentKey, "CMC role") } return nil diff --git a/lib/teleterm/teleterm.go b/lib/teleterm/teleterm.go index c8bc92b4e0e8a..0c6cde8efd031 100644 --- a/lib/teleterm/teleterm.go +++ b/lib/teleterm/teleterm.go @@ -20,6 +20,7 @@ package teleterm import ( "context" + "log/slog" "os" "os/signal" "path/filepath" @@ -28,7 +29,6 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - log "github.com/sirupsen/logrus" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -112,9 +112,9 @@ func Serve(ctx context.Context, cfg Config) error { select { case <-ctx.Done(): - log.Info("Context closed, stopping service.") + slog.InfoContext(ctx, "Context closed, stopping service") case sig := <-c: - log.Infof("Captured %s, stopping service.", sig) + slog.InfoContext(ctx, "Captured signal, stopping service", "signal", sig) } daemonService.Stop() diff --git a/tool/tsh/common/db.go b/tool/tsh/common/db.go index d26a215a8b4e4..baf222ab69f90 100644 --- a/tool/tsh/common/db.go +++ b/tool/tsh/common/db.go @@ -539,7 +539,7 @@ func onDatabaseConfig(cf *CLIConf) error { case dbFormatCommand: cmd, err := dbcmd.NewCmdBuilder(tc, profile, *database, rootCluster, dbcmd.WithPrintFormat(), - dbcmd.WithLogger(log), + dbcmd.WithLogger(logger), dbcmd.WithGetDatabaseFunc(getDatabase), ).GetConnectCommand(cf.Context) if err != nil { @@ -779,7 +779,7 @@ func onDatabaseConnect(cf *CLIConf) error { return trace.Wrap(err) } opts = append(opts, - dbcmd.WithLogger(log), + dbcmd.WithLogger(logger), dbcmd.WithGetDatabaseFunc(dbInfo.getDatabaseForDBCmd), ) diff --git a/tool/tsh/common/proxy.go b/tool/tsh/common/proxy.go index 4f0f1fee92135..489c3f483846f 100644 --- a/tool/tsh/common/proxy.go +++ b/tool/tsh/common/proxy.go @@ -233,7 +233,7 @@ func onProxyCommandDB(cf *CLIConf) error { opts := []dbcmd.ConnectCommandFunc{ dbcmd.WithLocalProxy("localhost", addr.Port(0), ""), dbcmd.WithNoTLS(), - dbcmd.WithLogger(log), + dbcmd.WithLogger(logger), dbcmd.WithPrintFormat(), dbcmd.WithTolerateMissingCLIClient(), dbcmd.WithGetDatabaseFunc(dbInfo.getDatabaseForDBCmd), From e135f9f3818ddf6c3c1adc0e1109a916adba2746 Mon Sep 17 00:00:00 2001 From: Tiago Silva Date: Wed, 18 Dec 2024 22:49:41 +0000 Subject: [PATCH 14/64] Relax `validServerHostname` to accept `--` or `..` (#50386) * Relax `validServerHostname` to accept `--` or `..` This PR updates the regex used by `validServerHostname` to permit consecutive symbols (`-` and `.`) in server hostnames. Changelog: Enable multiple consecutive occurrences of `-` and `.` in SSH server hostnames. Signed-off-by: Tiago Silva * revert to old hostname if it was valid under the new rules * fix * run test in parallel * Update lib/auth/auth.go Co-authored-by: Edoardo Spadolini * handle review comments --------- Signed-off-by: Tiago Silva Co-authored-by: Edoardo Spadolini --- lib/auth/auth.go | 42 +++++++++++++++++++---- lib/auth/auth_test.go | 80 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 111 insertions(+), 11 deletions(-) diff --git a/lib/auth/auth.go b/lib/auth/auth.go index c240ad6fc585f..4a88c5e083603 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -1508,12 +1508,11 @@ func (a *Server) runPeriodicOperations() { heartbeatsMissedByAuth.Inc() } + if srv.GetSubKind() != types.SubKindOpenSSHNode { + return false, nil + } // TODO(tross) DELETE in v20.0.0 - all invalid hostnames should have been sanitized by then. if !validServerHostname(srv.GetHostname()) { - if srv.GetSubKind() != types.SubKindOpenSSHNode { - return false, nil - } - logger := a.logger.With("server", srv.GetName(), "hostname", srv.GetHostname()) logger.DebugContext(a.closeCtx, "sanitizing invalid static SSH server hostname") @@ -1527,6 +1526,17 @@ func (a *Server) runPeriodicOperations() { if _, err := a.Services.UpdateNode(a.closeCtx, srv); err != nil && !trace.IsCompareFailed(err) { logger.WarnContext(a.closeCtx, "failed to update SSH server hostname", "error", err) } + } else if oldHostname, ok := srv.GetLabel(replacedHostnameLabel); ok && validServerHostname(oldHostname) { + // If the hostname has been replaced by a sanitized version, revert it back to the original + // if the original is valid under the most recent rules. + logger := a.logger.With("server", srv.GetName(), "old_hostname", oldHostname, "sanitized_hostname", srv.GetHostname()) + if err := restoreSanitizedHostname(srv); err != nil { + logger.WarnContext(a.closeCtx, "failed to restore sanitized static SSH server hostname", "error", err) + return false, nil + } + if _, err := a.Services.UpdateNode(a.closeCtx, srv); err != nil && !trace.IsCompareFailed(err) { + log.Warnf("Failed to update node hostname: %v", err) + } } return false, nil @@ -5650,7 +5660,7 @@ func (a *Server) KeepAliveServer(ctx context.Context, h types.KeepAlive) error { const ( serverHostnameMaxLen = 256 - serverHostnameRegexPattern = `^[a-zA-Z0-9]([\.-]?[a-zA-Z0-9]+)*$` + serverHostnameRegexPattern = `^[a-zA-Z0-9]+[a-zA-Z0-9\.-]*$` replacedHostnameLabel = types.TeleportInternalLabelPrefix + "invalid-hostname" ) @@ -5658,7 +5668,7 @@ var serverHostnameRegex = regexp.MustCompile(serverHostnameRegexPattern) // validServerHostname returns false if the hostname is longer than 256 characters or // does not entirely consist of alphanumeric characters as well as '-' and '.'. A valid hostname also -// cannot begin with a symbol, and a symbol cannot be followed immediately by another symbol. +// cannot begin with a symbol. func validServerHostname(hostname string) bool { return len(hostname) <= serverHostnameMaxLen && serverHostnameRegex.MatchString(hostname) } @@ -5697,6 +5707,26 @@ func sanitizeHostname(server types.Server) error { return nil } +// restoreSanitizedHostname restores the original hostname of a server and removes the label. +func restoreSanitizedHostname(server types.Server) error { + oldHostname, ok := server.GetLabels()[replacedHostnameLabel] + // if the label is not present or the hostname is invalid under the most recent rules, do nothing. + if !ok || !validServerHostname(oldHostname) { + return nil + } + + switch s := server.(type) { + case *types.ServerV2: + // restore the original hostname and remove the label. + s.Spec.Hostname = oldHostname + delete(s.Metadata.Labels, replacedHostnameLabel) + default: + return trace.BadParameter("invalid server provided") + } + + return nil +} + // UpsertNode implements [services.Presence] by delegating to [Server.Services] // and potentially emitting a [usagereporter] event. func (a *Server) UpsertNode(ctx context.Context, server types.Server) (*types.KeepAlive, error) { diff --git a/lib/auth/auth_test.go b/lib/auth/auth_test.go index e4978e32e358a..8f535a1727588 100644 --- a/lib/auth/auth_test.go +++ b/lib/auth/auth_test.go @@ -4478,6 +4478,10 @@ func TestServerHostnameSanitization(t *testing.T) { name: "uuid dns hostname", hostname: uuid.NewString() + ".example.com", }, + { + name: "valid dns hostname with multi-dots", + hostname: "llama..example.com", + }, { name: "empty hostname", hostname: "", @@ -4488,11 +4492,6 @@ func TestServerHostnameSanitization(t *testing.T) { hostname: strings.Repeat("a", serverHostnameMaxLen*2), invalidHostname: true, }, - { - name: "invalid dns hostname", - hostname: "llama..example.com", - invalidHostname: true, - }, { name: "spaces in hostname", hostname: "the quick brown fox jumps over the lazy dog", @@ -4562,3 +4561,74 @@ func TestServerHostnameSanitization(t *testing.T) { }) } } + +func TestValidServerHostname(t *testing.T) { + t.Parallel() + tests := []struct { + name string + hostname string + want bool + }{ + { + name: "valid dns hostname", + hostname: "llama.example.com", + want: true, + }, + { + name: "valid friendly hostname", + hostname: "llama", + want: true, + }, + { + name: "uuid hostname", + hostname: uuid.NewString(), + want: true, + }, + { + name: "valid hostname with multi-dashes", + hostname: "llama--example.com", + want: true, + }, + { + name: "valid hostname with multi-dots", + hostname: "llama..example.com", + want: true, + }, + { + name: "valid hostname with numbers", + hostname: "llama9", + want: true, + }, + { + name: "hostname with invalid characters", + hostname: "llama?!$", + want: false, + }, + { + name: "super long hostname", + hostname: strings.Repeat("a", serverHostnameMaxLen*2), + want: false, + }, + { + name: "hostname with spaces", + hostname: "the quick brown fox jumps over the lazy dog", + want: false, + }, + { + name: "hostname with ;", + hostname: "llama;example.com", + want: false, + }, + { + name: "empty hostname", + hostname: "", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := validServerHostname(tt.hostname) + require.Equal(t, tt.want, got) + }) + } +} From e67a13e885f1e167fd3bfce35c7b22bd66c8ff8f Mon Sep 17 00:00:00 2001 From: Erik Tate Date: Wed, 18 Dec 2024 17:58:39 -0500 Subject: [PATCH 15/64] replacing documentation references to port_forwarding with ssh_port_forwarding and adding some additional context to the rbac reference (#50361) --- docs/pages/admin-guides/api/rbac.mdx | 12 +++++- .../enroll-resources/server-access/rbac.mdx | 14 ++++++- docs/pages/includes/role-spec.mdx | 14 ++++++- .../pages/reference/access-controls/roles.mdx | 2 +- .../terraform-provider/resources/role.mdx | 14 +++++-- examples/resources/admin.yaml | 6 ++- examples/resources/user.yaml | 6 ++- .../resources/teleport_role/resource.tf | 14 +++++-- integrations/terraform/reference.mdx | 38 ++++++++++++++++++- rfd/0007-rbac-oss.md | 6 ++- rfd/0008-application-access.md | 6 ++- 11 files changed, 113 insertions(+), 19 deletions(-) diff --git a/docs/pages/admin-guides/api/rbac.mdx b/docs/pages/admin-guides/api/rbac.mdx index 83b9ae16edc0b..f292d19a1b63e 100644 --- a/docs/pages/admin-guides/api/rbac.mdx +++ b/docs/pages/admin-guides/api/rbac.mdx @@ -859,7 +859,11 @@ spec: enabled: true max_session_ttl: 30h0m0s pin_source_ip: false - port_forwarding: true + ssh_port_forwarding: + remote: + enabled: true + local: + enabled: true record_session: default: best_effort desktop: true @@ -906,7 +910,11 @@ spec: enabled: true max_session_ttl: 30h0m0s pin_source_ip: false - port_forwarding: true + ssh_port_forwarding: + remote: + enabled: true + local: + enabled: true record_session: default: best_effort desktop: true diff --git a/docs/pages/enroll-resources/server-access/rbac.mdx b/docs/pages/enroll-resources/server-access/rbac.mdx index 6cc43636c5d27..036a91e7aa642 100644 --- a/docs/pages/enroll-resources/server-access/rbac.mdx +++ b/docs/pages/enroll-resources/server-access/rbac.mdx @@ -135,8 +135,18 @@ spec: create_host_user_mode: keep # forward_agent controls whether SSH agent forwarding is allowed forward_agent: true - # port_forwarding controls whether TCP port forwarding is allowed for SSH - port_forwarding: true + # ssh_port_forwarding controls which TCP port forwarding modes are allowed over SSH. This replaces + # the deprecated port_forwarding field, which did not differentiate between remote and local + # port forwarding modes. If you have any existing roles that allow forwarding by enabling the + # legacy port_forwarding field then the forwarding controls configured in ssh_port_forwarding will be + # ignored. + ssh_port_forwarding: + # configures remote port forwarding behavior + remote: + enabled: true + # configures local port forwarding behavior + local: + enabled: true # ssh_file_copy controls whether file copying (SCP/SFTP) is allowed. # Defaults to true. ssh_file_copy: false diff --git a/docs/pages/includes/role-spec.mdx b/docs/pages/includes/role-spec.mdx index ef780959cd30b..1a0f2cfeece0c 100644 --- a/docs/pages/includes/role-spec.mdx +++ b/docs/pages/includes/role-spec.mdx @@ -13,8 +13,18 @@ spec: max_session_ttl: 8h # forward_agent controls whether SSH agent forwarding is allowed forward_agent: true - # port_forwarding controls whether TCP port forwarding is allowed for SSH - port_forwarding: true + # ssh_port_forwarding controls which TCP port forwarding modes are allowed over SSH. This replaces + # the deprecated port_forwarding field, which did not differentiate between remote and local + # port forwarding modes. If you have any existing roles that allow forwarding by enabling the + # legacy port_forwarding field then the forwarding controls configured in ssh_port_forwarding will be + # ignored. + ssh_port_forwarding: + # configures remote port forwarding behavior + remote: + enabled: true + # configures local port forwarding behavior + local: + enabled: true # ssh_file_copy controls whether file copying (SCP/SFTP) is allowed. # Defaults to true. ssh_file_copy: false diff --git a/docs/pages/reference/access-controls/roles.mdx b/docs/pages/reference/access-controls/roles.mdx index 86029bff5012b..c67dd234b8642 100644 --- a/docs/pages/reference/access-controls/roles.mdx +++ b/docs/pages/reference/access-controls/roles.mdx @@ -52,7 +52,7 @@ user: | - | - | - | | `max_session_ttl` | Max. time to live (TTL) of a user's SSH certificates | The shortest TTL wins | | `forward_agent` | Allow SSH agent forwarding | Logical "OR" i.e. if any role allows agent forwarding, it's allowed | -| `port_forwarding` | Allow TCP port forwarding | Logical "OR" i.e. if any role allows port forwarding, it's allowed | +| `ssh_port_forwarding` | Allow TCP port forwarding | Logical "AND" i.e. if any role denies port forwarding, it's denied | | `ssh_file_copy` | Allow SCP/SFTP | Logical "AND" i.e. if all roles allows file copying, it's allowed | | `client_idle_timeout` | Forcefully terminate active sessions after an idle interval | The shortest timeout value wins, i.e. the most restrictive value is selected | | `disconnect_expired_cert` | Forcefully terminate active sessions when a client certificate expires | Logical "OR" i.e. evaluates to "yes" if at least one role requires session termination | diff --git a/docs/pages/reference/terraform-provider/resources/role.mdx b/docs/pages/reference/terraform-provider/resources/role.mdx index 3d573fa65646b..70d9c3edc0f1e 100644 --- a/docs/pages/reference/terraform-provider/resources/role.mdx +++ b/docs/pages/reference/terraform-provider/resources/role.mdx @@ -27,9 +27,17 @@ resource "teleport_role" "example" { spec = { options = { - forward_agent = false - max_session_ttl = "7m" - port_forwarding = false + forward_agent = false + max_session_ttl = "7m" + ssh_port_forwarding = { + remote = { + enabled = false + } + + local = { + enabled = false + } + } client_idle_timeout = "1h" disconnect_expired_cert = true permit_x11_forwarding = false diff --git a/examples/resources/admin.yaml b/examples/resources/admin.yaml index 2c8427a632a4a..acb170f290e18 100644 --- a/examples/resources/admin.yaml +++ b/examples/resources/admin.yaml @@ -28,5 +28,9 @@ spec: - network forward_agent: true max_session_ttl: 30h0m0s - port_forwarding: true + ssh_port_forwarding: + remote: + enabled: true + local: + enabled: true version: v3 diff --git a/examples/resources/user.yaml b/examples/resources/user.yaml index 07ab839b3286a..8f47afd3fc0e9 100644 --- a/examples/resources/user.yaml +++ b/examples/resources/user.yaml @@ -56,5 +56,9 @@ spec: - network forward_agent: true max_session_ttl: 30h0m0s - port_forwarding: true + ssh_port_forwarding: + remote: + enabled: true + local: + enabled: true version: v3 diff --git a/integrations/terraform/examples/resources/teleport_role/resource.tf b/integrations/terraform/examples/resources/teleport_role/resource.tf index c5ac6c920e5d9..c76e71248e962 100644 --- a/integrations/terraform/examples/resources/teleport_role/resource.tf +++ b/integrations/terraform/examples/resources/teleport_role/resource.tf @@ -13,9 +13,17 @@ resource "teleport_role" "example" { spec = { options = { - forward_agent = false - max_session_ttl = "7m" - port_forwarding = false + forward_agent = false + max_session_ttl = "7m" + ssh_port_forwarding = { + remote = { + enabled = false + } + + local = { + enabled = false + } + } client_idle_timeout = "1h" disconnect_expired_cert = true permit_x11_forwarding = false diff --git a/integrations/terraform/reference.mdx b/integrations/terraform/reference.mdx index 078a1bb73ca08..a5df9cc060e46 100755 --- a/integrations/terraform/reference.mdx +++ b/integrations/terraform/reference.mdx @@ -2051,7 +2051,8 @@ Options is for OpenSSH options like agent forwarding. | max_sessions | number | | MaxSessions defines the maximum number of concurrent sessions per connection. | | permit_x11_forwarding | bool | | PermitX11Forwarding authorizes use of X11 forwarding. | | pin_source_ip | bool | | PinSourceIP forces the same client IP for certificate generation and usage | -| port_forwarding | bool | | | +| ssh_port_forwarding | object | | SSHPortForwarding configures what types of SSH port forwarding are allowed by a role. | +| port_forwarding | bool | | Deprecated: Use SSHPortForwarding instead. | | record_session | object | | RecordDesktopSession indicates whether desktop access sessions should be recorded. It defaults to true unless explicitly set to false. | | request_access | string | | RequestAccess defines the access request strategy (optional|note|always) where optional is the default. | | request_prompt | string | | RequestPrompt is an optional message which tells users what they aught to request. | @@ -2085,6 +2086,31 @@ SAML are options related to the Teleport SAML IdP. |---------|------|----------|-------------| | enabled | bool | | | +##### spec.options.ssh_port_forwarding + +SSHPortForwarding configures what types of SSH port forwarding are allowed by a role. + +| Name | Type | Required | Description | +|--------|--------|----------|-----------------------------------------------------------| +| remote | object | | remote contains options related to remote port forwarding | +| local | object | | local contains options related to local port forwarding | + +###### spec.options.ssh_port_forwarding.remote + +remote contains options related to remote port forwarding + +| Name | Type | Required | Description | +|---------|------|----------|-------------| +| enabled | bool | | | + +###### spec.options.ssh_port_forwarding.local + +local contains options related to local port forwarding + +| Name | Type | Required | Description | +|---------|------|----------|-------------| +| enabled | bool | | | + ##### spec.options.record_session RecordDesktopSession indicates whether desktop access sessions should be recorded. It defaults to true unless explicitly set to false. @@ -2114,11 +2140,19 @@ resource "teleport_role" "example" { options = { forward_agent = false max_session_ttl = "7m" - port_forwarding = false client_idle_timeout = "1h" disconnect_expired_cert = true permit_x11_forwarding = false request_access = "denied" + ssh_port_forwarding = { + remote = { + enabled = false + } + + local = { + enabled = false + } + } } allow = { diff --git a/rfd/0007-rbac-oss.md b/rfd/0007-rbac-oss.md index 33848f7780105..23d33cb443f8d 100644 --- a/rfd/0007-rbac-oss.md +++ b/rfd/0007-rbac-oss.md @@ -90,7 +90,11 @@ role: name: user spec: options: - port_forwarding: true + ssh_port_forwarding: + remote: + enabled: true + local: + enabled: true max_session_ttl: 30h forward_agent: true enhanced_recording: ['command', 'network'] diff --git a/rfd/0008-application-access.md b/rfd/0008-application-access.md index 89ec8b837a3fd..adf15b34e8ede 100644 --- a/rfd/0008-application-access.md +++ b/rfd/0008-application-access.md @@ -303,7 +303,11 @@ version: v3 spec: options: forward_agent: true - port_forwarding: false + ssh_port_forwarding: + remote: + enabled: false + local: + enabled: false allow: logins: ["rjones"] # Application labels define labels that an application must match for this From a3f0fdbbdacfde9ec2c55d09110fe05f213a29bf Mon Sep 17 00:00:00 2001 From: Tim Buckley Date: Wed, 18 Dec 2024 17:32:22 -0700 Subject: [PATCH 16/64] Fix absence of the Symlinks parameter for Destination when using tbot kube credentials (#50370) Signed-off-by: Tim Buckley Co-authored-by: Vadim Aleksandrov --- tool/tbot/kube.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tool/tbot/kube.go b/tool/tbot/kube.go index 2314ec83bc984..997b668087e11 100644 --- a/tool/tbot/kube.go +++ b/tool/tbot/kube.go @@ -73,6 +73,10 @@ func onKubeCredentialsCommand( return trace.Wrap(err) } + if err = destination.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + idData, err := destination.Read(ctx, config.IdentityFilePath) if err != nil { return trace.Wrap(err) From ba35baa30d0ba60edf5e2a54c8726abe31fa9de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Skrz=C4=99tnicki?= Date: Thu, 19 Dec 2024 09:13:03 +0100 Subject: [PATCH 17/64] Detail DAC and TAG integration from the user perspective (#46069) * Detail DAC and TAG integration from the user perspective * Change wording to make linter happy. * Fix broken links and spacing * Update docs/pages/admin-guides/teleport-policy/database-access-controls.mdx Co-authored-by: STeve (Xin) Huang * Update screenshots. Drop redundant instructions and merge the information into an existing suitable page. --------- Co-authored-by: STeve (Xin) Huang --- .../dac/db-object-contains-relation.png | Bin 0 -> 100141 bytes .../access-graph/dac/db-object-details.png | Bin 0 -> 146799 bytes .../dac/db-object-permissions-label.png | Bin 0 -> 109671 bytes docs/img/access-graph/dac/overview.png | Bin 0 -> 221286 bytes .../teleport-policy/policy-connections.mdx | 26 ++++++++++++++++++ 5 files changed, 26 insertions(+) create mode 100644 docs/img/access-graph/dac/db-object-contains-relation.png create mode 100644 docs/img/access-graph/dac/db-object-details.png create mode 100644 docs/img/access-graph/dac/db-object-permissions-label.png create mode 100644 docs/img/access-graph/dac/overview.png diff --git a/docs/img/access-graph/dac/db-object-contains-relation.png b/docs/img/access-graph/dac/db-object-contains-relation.png new file mode 100644 index 0000000000000000000000000000000000000000..c26f2c541bdaf311c28e7776f9dd53c63022fbb0 GIT binary patch literal 100141 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℞}yE^ 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уq3W# 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 literal 0 HcmV?d00001 diff --git a/docs/img/access-graph/dac/db-object-details.png b/docs/img/access-graph/dac/db-object-details.png new file mode 100644 index 0000000000000000000000000000000000000000..b0ffdd835149d17e6949c60594d4606fc57b8cd9 GIT binary patch literal 146799 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 z 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�Gi7pN{LdRePn9I5h)zK1bG= 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%0DZgU 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 literal 0 HcmV?d00001 diff --git a/docs/img/access-graph/dac/overview.png b/docs/img/access-graph/dac/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..47100c9188ee647a33cec4e62512effec666022d GIT binary patch literal 221286 zcmeEucQjmY_pcBnN=O9JLx@C8B#7wIi4vj?M)cl$nF)d*h#p2uwCKH;iHKfC?@Wl! zAj&9X%ssyE@7{HPYu$IPcm0w-@2nN)WH|dg=Xv(t&)%Q?*(Y2>O@W+*o&*mMk6cMn zRud18LI@8JU-KFf@C}2)cL*LHnVOA^jE0ho%v}vvCrcZ93p_l<@I+l=J*^&^43H-4 z)oZd+%Im6>vA3j@ukjv6-{oSxPO1Fix^zc0U+GVSn>Ts58%e%QUAf7yS;!q}>ctyL z7?SAWVzk{0qm)VxSn)dua9e}p(oWaX+^lw52=H3s`4d;aq~a|r?ORl|T@?59Jmk?7 z)*v8~B3;L41K&+{{gT~&g{K{b?AwCjy{esLOfbN0kI2Nqu2eqwOX1p=2S# zbGoT$af(No`%ySEkzadB;Jt_Wko0@IglAeIpoC{eA!TGUKe}95A^3Ru>4;%L;z@HAO+4fnDO$LG@!u_#jfMjH_q@`KAP)W~MYrIn)Vz4kEs2!S8c$_$?*t6!uc zBE$43mgr-}9yxZeCfLhw?==fQF*h8fL=GgApJk2Vio2D5em#7Q;<@Rh$l}J+AE6%f ziNr-J@Y&;ArsVJMt%$h1kr65g(Vy#n!;r$JLl;T-Lm-xY3Y;60Xw@BL8n*4V))Qvd zrDhTC6DxCK3(@g^&!sy>bp<*Ey+b;|8RvW})I)Z^XR*EFn%tGZ!YP+Z#W>3RB^Pm< zR%o#;k0a+m6mw-4R<5BQ&lPpnL4r4TX&1PO64~!Dvj=g0yoqbSCsB-lM~1Js z_X3Kbr~==)Vg5brPRHH{p0jd}#~<%Vk+BlpbZ{Y
    A#zBo>yW0e-=N7nVbBV#XN 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 z%0D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 z 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

    Q_vzA#ZDsg~ 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 literal 0 HcmV?d00001 diff --git a/docs/pages/admin-guides/teleport-policy/policy-connections.mdx b/docs/pages/admin-guides/teleport-policy/policy-connections.mdx index 37fae4539620d..85120c116e6f0 100644 --- a/docs/pages/admin-guides/teleport-policy/policy-connections.mdx +++ b/docs/pages/admin-guides/teleport-policy/policy-connections.mdx @@ -71,6 +71,32 @@ can be identified by having `Temporary: true` property. Resource Groups are created from Teleport roles. +### Database Access Controls + +Teleport supports [object-level permissions](../../enroll-resources/database-access/rbac.mdx#executing-database-object-permission-rules) for select database protocols. + +The database objects-level access information is automatically synchronized to Teleport Policy, making it possible to see who has particular levels of access to the different parts of the database. + +When you inspect a particular user's access, the Teleport Access Graph will automatically display the database objects that the user can access. + +![Overview of access including individual database objects](../../../img/access-graph/dac/overview.png) + +To see more details about a specific database object, simply select it. + +

    +![Details of an individual database object](../../../img/access-graph/dac/db-object-details.png) +
    + +In the graph, database objects are connected by multiple edges: + +1. There is exactly one edge connecting the object to its parent database resource. This edge has "contains" label. + +![Database object and parent database resource](../../../img/access-graph/dac/db-object-contains-relation.png) + +2. At least one edge shows the permissions associated with the object, such as `INSERT, SELECT, UPDATE`. If multiple roles grant permissions to the same object, additional edges of this type may be present. The permissions are presented as edge labels. + +![Specific object permissions](../../../img/access-graph/dac/db-object-permissions-label.png) + #### Resources Resources are created from Teleport resources like nodes, databases, and Kubernetes clusters. From a1881e9caddffeeb0f4ebaa7911fb8b8a039d2c6 Mon Sep 17 00:00:00 2001 From: Alan Parra Date: Thu, 19 Dec 2024 10:48:05 -0300 Subject: [PATCH 18/64] chore: Bump golang.org/x/net to v0.33.0 (#50396) --- api/go.mod | 2 +- api/go.sum | 4 ++-- assets/backport/go.mod | 2 +- assets/backport/go.sum | 4 ++-- build.assets/tooling/go.mod | 2 +- build.assets/tooling/go.sum | 4 ++-- examples/access-plugin-minimal/go.mod | 2 +- examples/access-plugin-minimal/go.sum | 4 ++-- examples/api-sync-roles/go.mod | 2 +- examples/api-sync-roles/go.sum | 4 ++-- examples/desktop-registration/go.mod | 2 +- examples/desktop-registration/go.sum | 4 ++-- examples/go-client/go.mod | 2 +- examples/go-client/go.sum | 4 ++-- examples/service-discovery-api-client/go.mod | 2 +- examples/service-discovery-api-client/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- integrations/event-handler/go.mod | 2 +- integrations/event-handler/go.sum | 4 ++-- integrations/terraform/go.mod | 2 +- integrations/terraform/go.sum | 4 ++-- 22 files changed, 33 insertions(+), 33 deletions(-) diff --git a/api/go.mod b/api/go.mod index 57f739e6ad14b..e01ed6bbbfcd6 100644 --- a/api/go.mod +++ b/api/go.mod @@ -24,7 +24,7 @@ require ( go.opentelemetry.io/proto/otlp v1.4.0 golang.org/x/crypto v0.31.0 golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f - golang.org/x/net v0.31.0 + golang.org/x/net v0.33.0 golang.org/x/term v0.27.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 google.golang.org/grpc v1.68.0 diff --git a/api/go.sum b/api/go.sum index da681baa3b07d..4c35d634358ec 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1058,8 +1058,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/assets/backport/go.mod b/assets/backport/go.mod index 1a62feb772b02..d6e5e4badd709 100644 --- a/assets/backport/go.mod +++ b/assets/backport/go.mod @@ -15,6 +15,6 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/net v0.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/assets/backport/go.sum b/assets/backport/go.sum index 7d0946671e57b..ab21f712120e0 100644 --- a/assets/backport/go.sum +++ b/assets/backport/go.sum @@ -995,8 +995,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/build.assets/tooling/go.mod b/build.assets/tooling/go.mod index cfc1305c2b386..1f54c7da795e5 100644 --- a/build.assets/tooling/go.mod +++ b/build.assets/tooling/go.mod @@ -54,7 +54,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect diff --git a/build.assets/tooling/go.sum b/build.assets/tooling/go.sum index c4ff39d6d8447..8db0e9b8359de 100644 --- a/build.assets/tooling/go.sum +++ b/build.assets/tooling/go.sum @@ -1114,8 +1114,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/examples/access-plugin-minimal/go.mod b/examples/access-plugin-minimal/go.mod index 85aee3c5a99da..278b61fe25b2c 100644 --- a/examples/access-plugin-minimal/go.mod +++ b/examples/access-plugin-minimal/go.mod @@ -48,7 +48,7 @@ require ( go.opentelemetry.io/proto/otlp v1.1.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect diff --git a/examples/access-plugin-minimal/go.sum b/examples/access-plugin-minimal/go.sum index fe8f19d63479a..7eb7ac9bd2334 100644 --- a/examples/access-plugin-minimal/go.sum +++ b/examples/access-plugin-minimal/go.sum @@ -203,8 +203,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= diff --git a/examples/api-sync-roles/go.mod b/examples/api-sync-roles/go.mod index e4134f2934472..28c2374d1c3d4 100644 --- a/examples/api-sync-roles/go.mod +++ b/examples/api-sync-roles/go.mod @@ -59,7 +59,7 @@ require ( go.opentelemetry.io/proto/otlp v1.1.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect diff --git a/examples/api-sync-roles/go.sum b/examples/api-sync-roles/go.sum index be4b25422f0cd..288a6104afcbe 100644 --- a/examples/api-sync-roles/go.sum +++ b/examples/api-sync-roles/go.sum @@ -234,8 +234,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= diff --git a/examples/desktop-registration/go.mod b/examples/desktop-registration/go.mod index 5da92f124caa3..6ad053d7b9e9e 100644 --- a/examples/desktop-registration/go.mod +++ b/examples/desktop-registration/go.mod @@ -38,7 +38,7 @@ require ( go.opentelemetry.io/proto/otlp v1.1.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/examples/desktop-registration/go.sum b/examples/desktop-registration/go.sum index 0b5eedd0047a0..d42087e9f4793 100644 --- a/examples/desktop-registration/go.sum +++ b/examples/desktop-registration/go.sum @@ -185,8 +185,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= diff --git a/examples/go-client/go.mod b/examples/go-client/go.mod index f843df505b16b..6cb0bf4510302 100644 --- a/examples/go-client/go.mod +++ b/examples/go-client/go.mod @@ -40,7 +40,7 @@ require ( go.opentelemetry.io/proto/otlp v1.1.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/examples/go-client/go.sum b/examples/go-client/go.sum index 0b5eedd0047a0..d42087e9f4793 100644 --- a/examples/go-client/go.sum +++ b/examples/go-client/go.sum @@ -185,8 +185,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= diff --git a/examples/service-discovery-api-client/go.mod b/examples/service-discovery-api-client/go.mod index 5cfe290b880fb..004b46d0f3fe3 100644 --- a/examples/service-discovery-api-client/go.mod +++ b/examples/service-discovery-api-client/go.mod @@ -56,7 +56,7 @@ require ( golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.25.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect diff --git a/examples/service-discovery-api-client/go.sum b/examples/service-discovery-api-client/go.sum index 2511aa0d9ec69..55c047d98343e 100644 --- a/examples/service-discovery-api-client/go.sum +++ b/examples/service-discovery-api-client/go.sum @@ -214,8 +214,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= diff --git a/go.mod b/go.mod index 4c2145a3afd93..214e7e3e1714f 100644 --- a/go.mod +++ b/go.mod @@ -201,7 +201,7 @@ require ( golang.org/x/crypto v0.31.0 golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f golang.org/x/mod v0.22.0 - golang.org/x/net v0.31.0 + golang.org/x/net v0.33.0 golang.org/x/oauth2 v0.24.0 golang.org/x/sync v0.10.0 golang.org/x/sys v0.28.0 diff --git a/go.sum b/go.sum index e0b7780b45836..82132a3ee7268 100644 --- a/go.sum +++ b/go.sum @@ -2513,8 +2513,8 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/integrations/event-handler/go.mod b/integrations/event-handler/go.mod index 218c2a5118595..56ac4bd95bd73 100644 --- a/integrations/event-handler/go.mod +++ b/integrations/event-handler/go.mod @@ -15,7 +15,7 @@ require ( github.com/peterbourgon/diskv/v3 v3.0.1 github.com/sethvargo/go-limiter v1.0.0 github.com/stretchr/testify v1.10.0 - golang.org/x/net v0.31.0 + golang.org/x/net v0.33.0 golang.org/x/time v0.8.0 google.golang.org/protobuf v1.36.0 ) diff --git a/integrations/event-handler/go.sum b/integrations/event-handler/go.sum index 08fc1add428b4..2b0f134829fcd 100644 --- a/integrations/event-handler/go.sum +++ b/integrations/event-handler/go.sum @@ -1795,8 +1795,8 @@ golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/integrations/terraform/go.mod b/integrations/terraform/go.mod index b2bd7f4e5536f..e6e5624117945 100644 --- a/integrations/terraform/go.mod +++ b/integrations/terraform/go.mod @@ -352,7 +352,7 @@ require ( golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect diff --git a/integrations/terraform/go.sum b/integrations/terraform/go.sum index 92822ceaf79ee..e2d3ece037acc 100644 --- a/integrations/terraform/go.sum +++ b/integrations/terraform/go.sum @@ -2140,8 +2140,8 @@ golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= From 48f7e1afe00c89928b60be4021e3aecb8a308fe0 Mon Sep 17 00:00:00 2001 From: Steven Martin Date: Thu, 19 Dec 2024 09:49:04 -0500 Subject: [PATCH 19/64] docs: remove references to enterprise version of tctl (#50419) --- .../access-request-plugins/opsgenie.mdx | 4 ++-- .../admin-guides/management/admin/trustedclusters.mdx | 6 ++---- .../kubernetes-access/getting-started.mdx | 10 ++++------ docs/pages/reference/cloud-faq.mdx | 2 +- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/docs/pages/admin-guides/access-controls/access-request-plugins/opsgenie.mdx b/docs/pages/admin-guides/access-controls/access-request-plugins/opsgenie.mdx index 0492e31ce28c1..01e97b95c59f1 100644 --- a/docs/pages/admin-guides/access-controls/access-request-plugins/opsgenie.mdx +++ b/docs/pages/admin-guides/access-controls/access-request-plugins/opsgenie.mdx @@ -20,13 +20,13 @@ Opsgenie. - A Teleport Enterprise Cloud account. -- The Enterprise `tctl` admin tool and `tsh` client tool version >= (=teleport.version=). +- The `tctl` admin tool and `tsh` client tool version >= (=teleport.version=). You can verify the tools you have installed by running the following commands: ```code $ tctl version - # Teleport Enterprise v(=teleport.version=) go(=teleport.golang=) + # Teleport v(=teleport.version=) go(=teleport.golang=) $ tsh version # Teleport v(=teleport.version=) go(=teleport.golang=) diff --git a/docs/pages/admin-guides/management/admin/trustedclusters.mdx b/docs/pages/admin-guides/management/admin/trustedclusters.mdx index 5322f020927f0..ae6bbe6428094 100644 --- a/docs/pages/admin-guides/management/admin/trustedclusters.mdx +++ b/docs/pages/admin-guides/management/admin/trustedclusters.mdx @@ -151,13 +151,11 @@ To complete the steps in this guide, verify your environment meets the following - The `tctl` admin tool and `tsh` client tool version >= (=teleport.version=). - For Teleport Enterprise and Teleport Enterprise cloud, you should have the - Enterprise version of `tctl` and `tsh` installed. You can verify the tools - you have installed by running the following commands: + You can verify the tools you have installed by running the following commands: ```code $ tctl version - # Teleport Enterprise v(=teleport.version=) go(=teleport.golang=) + # Teleport v(=teleport.version=) go(=teleport.golang=) $ tsh version # Teleport v(=teleport.version=) go(=teleport.golang=) diff --git a/docs/pages/enroll-resources/kubernetes-access/getting-started.mdx b/docs/pages/enroll-resources/kubernetes-access/getting-started.mdx index 552bed0c763f4..e7ced2de49bd4 100644 --- a/docs/pages/enroll-resources/kubernetes-access/getting-started.mdx +++ b/docs/pages/enroll-resources/kubernetes-access/getting-started.mdx @@ -22,19 +22,17 @@ For information about other ways to enroll and discover Kubernetes clusters, see - Access to a running Teleport cluster, `tctl` admin tool, and `tsh` client tool, version >= (=teleport.version=). - - For Teleport Enterprise and Teleport Enterprise Cloud, you should - use the Enterprise version of `tctl`. + You can verify the tools you have installed by running the following commands: ```code $ tctl version - # Teleport Enterprise v(=teleport.version=) go(=teleport.golang=) - + # Teleport v(=teleport.version=) go(=teleport.golang=) + $ tsh version # Teleport v(=teleport.version=) go(=teleport.golang=) ``` - + You can download these tools by following the appropriate [Installation instructions](../../installation.mdx#linux) for your environment. diff --git a/docs/pages/reference/cloud-faq.mdx b/docs/pages/reference/cloud-faq.mdx index c6ce43df8e05b..d39fa4107e3f3 100644 --- a/docs/pages/reference/cloud-faq.mdx +++ b/docs/pages/reference/cloud-faq.mdx @@ -95,7 +95,7 @@ $ tctl nodes add --ttl=5m --roles=node,proxy --token=$(uuid) ### How can I access the `tctl` admin tool? -Find the appropriate download at [Installation](../installation.mdx). Use the Enterprise version of `tctl`. +Find the appropriate download at [Installation](../installation.mdx). After downloading the tools, first log in to your cluster using `tsh`, then use `tctl` remotely: From a51b78b4e6de25e3d928cb02e9aa890f9c8ae053 Mon Sep 17 00:00:00 2001 From: Paul Gottschling Date: Thu, 19 Dec 2024 09:49:44 -0500 Subject: [PATCH 20/64] Fix TTL mistake in a partial (#50401) Closes #47068 The TTL shown in a `tctl auth sign` command does not correspond to the description of the TTL. Fix the description while making the TTL an integer divisible by 24. --- .../pages/includes/database-access/tctl-auth-sign-3-files.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/pages/includes/database-access/tctl-auth-sign-3-files.mdx b/docs/pages/includes/database-access/tctl-auth-sign-3-files.mdx index 253c54b2121b4..a84fab0c7d9ad 100644 --- a/docs/pages/includes/database-access/tctl-auth-sign-3-files.mdx +++ b/docs/pages/includes/database-access/tctl-auth-sign-3-files.mdx @@ -30,12 +30,12 @@ the database, follow these instructions on your workstation: ``` 1. Export Teleport's certificate authority and a generate certificate/key pair. - This example generates a certificate with a 1-year validity period. + This example generates a certificate with a 90-day validity period. `db.example.com` is the hostname where the Teleport Database Service can reach the {{ dbname }} server. ```code - $ tctl auth sign --format={{ format }} --host=db.example.com --out=server --ttl=2190h + $ tctl auth sign --format={{ format }} --host=db.example.com --out=server --ttl=2160h ``` (!docs/pages/includes/database-access/ttl-note.mdx!) From 50e27fd6bf1f38565523a33bef37aa6a72211068 Mon Sep 17 00:00:00 2001 From: Alan Parra Date: Thu, 19 Dec 2024 12:07:33 -0300 Subject: [PATCH 21/64] chore: Bump e/ reference (#50438) --- e | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e b/e index 5ab219dde2a8d..ffbb6c77ab986 160000 --- a/e +++ b/e @@ -1 +1 @@ -Subproject commit 5ab219dde2a8dbdac68823f084d94cb7c0223aeb +Subproject commit ffbb6c77ab986eae88ed8681c4b67b0e82820eae From 05be924ae05c433d2867c9e05f3beeb5fd1bae42 Mon Sep 17 00:00:00 2001 From: rosstimothy <39066650+rosstimothy@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:08:04 -0500 Subject: [PATCH 22/64] Convert lib/utils to slog (#50340) This only converts the places in the utils package that were logging with logrus, but it does not fully remove the logrus package as a dependency. The package is home to the logrus text formatter which needs to exist until slog is the only logging mechanism in use. --- lib/utils/addr.go | 5 +-- lib/utils/agentconn/agent_windows.go | 5 +-- lib/utils/aws/credentials.go | 9 ++++-- lib/utils/config.go | 5 +-- lib/utils/diagnostics/latency/monitor.go | 13 ++++---- lib/utils/envutils/environment.go | 20 +++++++----- lib/utils/fanoutbuffer/buffer.go | 4 +-- lib/utils/host/hostusers.go | 25 +++++++++++---- lib/utils/loadbalancer.go | 39 ++++++++++++------------ lib/utils/proxy/proxy.go | 12 ++++---- lib/utils/proxyconn_test.go | 8 ++--- lib/utils/registry/registry_windows.go | 38 ++++++++++++++++++----- lib/utils/socks/socks.go | 7 ----- lib/utils/socks/socks_test.go | 10 +++--- lib/utils/typical/cached_parser.go | 16 +++++----- lib/utils/typical/cached_parser_test.go | 20 ------------ lib/utils/unpack.go | 8 +++-- lib/utils/utils.go | 10 ++++-- 18 files changed, 142 insertions(+), 112 deletions(-) diff --git a/lib/utils/addr.go b/lib/utils/addr.go index b0ab5e4c4e258..3ed10bec068b5 100644 --- a/lib/utils/addr.go +++ b/lib/utils/addr.go @@ -19,7 +19,9 @@ package utils import ( + "context" "fmt" + "log/slog" "net" "net/url" "strconv" @@ -27,7 +29,6 @@ import ( "unicode/utf8" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" apiutils "github.com/gravitational/teleport/api/utils" ) @@ -290,7 +291,7 @@ func GuessHostIP() (ip net.IP, err error) { for _, iface := range ifaces { ifadrs, err := iface.Addrs() if err != nil { - log.Warn(err) + slog.WarnContext(context.Background(), "Unable to get addresses for interface", "interface", iface.Name, "error", err) } else { adrs = append(adrs, ifadrs...) } diff --git a/lib/utils/agentconn/agent_windows.go b/lib/utils/agentconn/agent_windows.go index d00eb81637ab2..850f791eb044a 100644 --- a/lib/utils/agentconn/agent_windows.go +++ b/lib/utils/agentconn/agent_windows.go @@ -22,8 +22,10 @@ package agentconn import ( + "context" "encoding/binary" "encoding/hex" + "log/slog" "net" "os" "os/exec" @@ -33,7 +35,6 @@ import ( "github.com/Microsoft/go-winio" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" apiutils "github.com/gravitational/teleport/api/utils" ) @@ -214,7 +215,7 @@ func getCygwinUIDFromPS() (uint32, error) { // preform a successful handshake with it. Handshake details here: // https://stackoverflow.com/questions/23086038/what-mechanism-is-used-by-msys-cygwin-to-emulate-unix-domain-sockets func attemptCygwinHandshake(port, key string, uid uint32) (net.Conn, error) { - logrus.Debugf("[KEY AGENT] attempting a handshake with Cygwin ssh-agent socket; port=%s uid=%d", port, uid) + slog.DebugContext(context.Background(), "[KEY AGENT] attempting a handshake with Cygwin ssh-agent socket", "port", port, "uid", uid) conn, err := net.Dial("tcp", "localhost:"+port) if err != nil { diff --git a/lib/utils/aws/credentials.go b/lib/utils/aws/credentials.go index 257d6606d42a9..47c3105174943 100644 --- a/lib/utils/aws/credentials.go +++ b/lib/utils/aws/credentials.go @@ -20,6 +20,7 @@ package aws import ( "context" + "log/slog" "sort" "strings" "time" @@ -33,7 +34,6 @@ import ( "github.com/aws/aws-sdk-go/service/sts" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/utils" @@ -70,8 +70,11 @@ func NewCredentialsGetter() CredentialsGetter { } // Get obtains STS credentials. -func (g *credentialsGetter) Get(_ context.Context, request GetCredentialsRequest) (*credentials.Credentials, error) { - logrus.Debugf("Creating STS session %q for %q.", request.SessionName, request.RoleARN) +func (g *credentialsGetter) Get(ctx context.Context, request GetCredentialsRequest) (*credentials.Credentials, error) { + slog.DebugContext(ctx, "Creating STS session", + "session_name", request.SessionName, + "role_arn", request.RoleARN, + ) return stscreds.NewCredentials(request.Provider, request.RoleARN, func(cred *stscreds.AssumeRoleProvider) { cred.RoleSessionName = MaybeHashRoleSessionName(request.SessionName) diff --git a/lib/utils/config.go b/lib/utils/config.go index 2a68f2efdce6c..969ccb8a8508b 100644 --- a/lib/utils/config.go +++ b/lib/utils/config.go @@ -19,12 +19,13 @@ package utils import ( + "context" + "log/slog" "os" "path/filepath" "strings" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" ) // TryReadValueAsFile is a utility function to read a value @@ -44,7 +45,7 @@ func TryReadValueAsFile(value string) (string, error) { out := strings.TrimSpace(string(contents)) if out == "" { - log.Warnf("Empty config value file: %v", value) + slog.WarnContext(context.Background(), "Empty config value file", "file", value) } return out, nil } diff --git a/lib/utils/diagnostics/latency/monitor.go b/lib/utils/diagnostics/latency/monitor.go index bbd2e98fa0782..cdfe08248c1f2 100644 --- a/lib/utils/diagnostics/latency/monitor.go +++ b/lib/utils/diagnostics/latency/monitor.go @@ -20,19 +20,20 @@ package latency import ( "context" + "errors" "sync/atomic" "time" "github.com/google/uuid" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/utils/retryutils" + logutils "github.com/gravitational/teleport/lib/utils/log" ) -var log = logrus.WithField(teleport.ComponentKey, "latency") +var logger = logutils.NewPackageLogger(teleport.ComponentKey, "latency") // Statistics contain latency measurements for both // legs of a proxied connection. @@ -188,8 +189,8 @@ func (m *Monitor) Run(ctx context.Context) { for { select { case <-m.reportTimer.Chan(): - if err := m.reporter.Report(ctx, m.GetStats()); err != nil { - log.WithError(err).Warn("failed to report latency stats") + if err := m.reporter.Report(ctx, m.GetStats()); err != nil && !errors.Is(err, context.Canceled) { + logger.WarnContext(ctx, "failed to report latency stats", "error", err) } m.reportTimer.Reset(retryutils.SeventhJitter(m.reportInterval)) case <-ctx.Done(): @@ -205,8 +206,8 @@ func (m *Monitor) pingLoop(ctx context.Context, pinger Pinger, timer clockwork.T return case <-timer.Chan(): then := m.clock.Now() - if err := pinger.Ping(ctx); err != nil { - log.WithError(err).Warn("unexpected failure sending ping") + if err := pinger.Ping(ctx); err != nil && !errors.Is(err, context.Canceled) { + logger.WarnContext(ctx, "unexpected failure sending ping", "error", err) } else { latency.Store(m.clock.Now().Sub(then).Milliseconds()) } diff --git a/lib/utils/envutils/environment.go b/lib/utils/envutils/environment.go index 19380be212f5f..ab3e34ad908cb 100644 --- a/lib/utils/envutils/environment.go +++ b/lib/utils/envutils/environment.go @@ -20,13 +20,13 @@ package envutils import ( "bufio" + "context" "fmt" "io" + "log/slog" "os" "strings" - log "github.com/sirupsen/logrus" - "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/utils" ) @@ -39,7 +39,10 @@ func ReadEnvironmentFile(filename string) ([]string, error) { // having this file for the user is optional. file, err := utils.OpenFileNoUnsafeLinks(filename) if err != nil { - log.Warnf("Unable to open environment file %v: %v, skipping", filename, err) + slog.WarnContext(context.Background(), "Unable to open environment file, skipping", + "file", filename, + "error", err, + ) return []string{}, nil } defer file.Close() @@ -51,6 +54,7 @@ func readEnvironment(r io.Reader) ([]string, error) { var lineno int env := &SafeEnv{} + ctx := context.Background() scanner := bufio.NewScanner(r) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) @@ -59,7 +63,9 @@ func readEnvironment(r io.Reader) ([]string, error) { // https://github.com/openssh/openssh-portable/blob/master/session.c#L873-L874 lineno = lineno + 1 if lineno > teleport.MaxEnvironmentFileLines { - log.Warnf("Too many lines in environment file, returning first %v lines", teleport.MaxEnvironmentFileLines) + slog.WarnContext(ctx, "Too many lines in environment file, limiting how many are consumed", + "lines_consumed", teleport.MaxEnvironmentFileLines, + ) return *env, nil } @@ -71,7 +77,7 @@ func readEnvironment(r io.Reader) ([]string, error) { // split on first =, if not found, log it and continue idx := strings.Index(line, "=") if idx == -1 { - log.Debugf("Bad line %v while reading environment file: no = separator found", lineno) + slog.DebugContext(ctx, "Bad line while reading environment file: no = separator found", "line_number", lineno) continue } @@ -79,7 +85,7 @@ func readEnvironment(r io.Reader) ([]string, error) { key := line[:idx] value := line[idx+1:] if strings.TrimSpace(key) == "" { - log.Debugf("Bad line %v while reading environment file: key without name", lineno) + slog.DebugContext(ctx, "Bad line while reading environment file: key without name", "line_number", lineno) continue } @@ -88,7 +94,7 @@ func readEnvironment(r io.Reader) ([]string, error) { } if err := scanner.Err(); err != nil { - log.Warnf("Unable to read environment file: %v", err) + slog.WarnContext(ctx, "Unable to read environment file", "error", err) return []string{}, nil } diff --git a/lib/utils/fanoutbuffer/buffer.go b/lib/utils/fanoutbuffer/buffer.go index 180ef0aa6bd4a..e7a1a600e935f 100644 --- a/lib/utils/fanoutbuffer/buffer.go +++ b/lib/utils/fanoutbuffer/buffer.go @@ -21,13 +21,13 @@ package fanoutbuffer import ( "context" "errors" + "log/slog" "runtime" "sync" "sync/atomic" "time" "github.com/jonboulle/clockwork" - log "github.com/sirupsen/logrus" ) // ErrGracePeriodExceeded is an error returned by Cursor.Read indicating that the cursor fell @@ -380,7 +380,7 @@ func finalizeCursor[T any](cursor *Cursor[T]) { } cursor.closeLocked() - log.Warn("Fanout buffer cursor was never closed. (this is a bug)") + slog.WarnContext(context.Background(), "Fanout buffer cursor was never closed. (this is a bug)") } // Close closes the cursor. Close is safe to double-call and should be called as soon as possible if diff --git a/lib/utils/host/hostusers.go b/lib/utils/host/hostusers.go index 3123d1a5d8879..de3ce20b5d69d 100644 --- a/lib/utils/host/hostusers.go +++ b/lib/utils/host/hostusers.go @@ -21,7 +21,9 @@ package host import ( "bufio" "bytes" + "context" "errors" + "log/slog" "os" "os/exec" "os/user" @@ -29,7 +31,6 @@ import ( "strings" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" ) // man GROUPADD(8), exit codes section @@ -55,7 +56,10 @@ func GroupAdd(groupname string, gid string) (exitCode int, err error) { cmd := exec.Command(groupaddBin, args...) output, err := cmd.CombinedOutput() - log.Debugf("%s output: %s", cmd.Path, string(output)) + slog.DebugContext(context.Background(), "groupadd command completed", + "command_path", cmd.Path, + "output", string(output), + ) switch code := cmd.ProcessState.ExitCode(); code { case GroupExistExit: @@ -122,7 +126,7 @@ func UserAdd(username string, groups []string, opts UserOpts) (exitCode int, err if opts.Shell != "" { if shell, err := exec.LookPath(opts.Shell); err != nil { - log.Warnf("configured shell %q not found, falling back to host default", opts.Shell) + slog.WarnContext(context.Background(), "configured shell not found, falling back to host default", "shell", opts.Shell) } else { args = append(args, "--shell", shell) } @@ -130,7 +134,10 @@ func UserAdd(username string, groups []string, opts UserOpts) (exitCode int, err cmd := exec.Command(useraddBin, args...) output, err := cmd.CombinedOutput() - log.Debugf("%s output: %s", cmd.Path, string(output)) + slog.DebugContext(context.Background(), "useradd command completed", + "command_path", cmd.Path, + "output", string(output), + ) if cmd.ProcessState.ExitCode() == UserExistExit { return cmd.ProcessState.ExitCode(), trace.AlreadyExists("user already exists") } @@ -147,7 +154,10 @@ func SetUserGroups(username string, groups []string) (exitCode int, err error) { // usermod -G (replace groups) (username) cmd := exec.Command(usermodBin, "-G", strings.Join(groups, ","), username) output, err := cmd.CombinedOutput() - log.Debugf("%s output: %s", cmd.Path, string(output)) + slog.DebugContext(context.Background(), "usermod completed", + "command_path", cmd.Path, + "output", string(output), + ) return cmd.ProcessState.ExitCode(), trace.Wrap(err) } @@ -170,7 +180,10 @@ func UserDel(username string) (exitCode int, err error) { // userdel --remove (remove home) username cmd := exec.Command(userdelBin, args...) output, err := cmd.CombinedOutput() - log.Debugf("%s output: %s", cmd.Path, string(output)) + slog.DebugContext(context.Background(), "usedel command completed", + "command_path", cmd.Path, + "output", string(output), + ) return cmd.ProcessState.ExitCode(), trace.Wrap(err) } diff --git a/lib/utils/loadbalancer.go b/lib/utils/loadbalancer.go index c201f8e60a79c..6a256e9113bbe 100644 --- a/lib/utils/loadbalancer.go +++ b/lib/utils/loadbalancer.go @@ -22,15 +22,16 @@ import ( "context" "errors" "io" + "log/slog" "math/rand/v2" "net" "sync" "time" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "github.com/gravitational/teleport" + logutils "github.com/gravitational/teleport/lib/utils/log" ) // NewLoadBalancer returns new load balancer listening on frontend @@ -58,12 +59,10 @@ func newLoadBalancer(ctx context.Context, frontend NetAddr, policy loadBalancerP policy: policy, waitCtx: waitCtx, waitCancel: waitCancel, - Entry: log.WithFields(log.Fields{ - teleport.ComponentKey: "loadbalancer", - teleport.ComponentFields: log.Fields{ - "listen": frontend.String(), - }, - }), + logger: slog.With( + teleport.ComponentKey, "loadbalancer", + "frontend_addr", frontend.FullAddress(), + ), connections: make(map[NetAddr]map[int64]net.Conn), }, nil } @@ -103,8 +102,8 @@ func randomPolicy() loadBalancerPolicy { // balancer used in tests. type LoadBalancer struct { sync.RWMutex - connID int64 - *log.Entry + connID int64 + logger *slog.Logger frontend NetAddr backends []NetAddr ctx context.Context @@ -156,7 +155,7 @@ func (l *LoadBalancer) AddBackend(b NetAddr) { l.Lock() defer l.Unlock() l.backends = append(l.backends, b) - l.Debugf("Backends %v.", l.backends) + l.logger.DebugContext(l.ctx, "Backends updated", "backends", l.backends) } // RemoveBackend removes backend @@ -205,7 +204,9 @@ func (l *LoadBalancer) Listen() error { if err != nil { return trace.ConvertSystemError(err) } - l.Debugf("created listening socket on %q", l.listener.Addr()) + l.logger.DebugContext(l.ctx, "created listening socket", + "listen_addr", logutils.StringerAttr(l.listener.Addr()), + ) return nil } @@ -231,7 +232,7 @@ func (l *LoadBalancer) Serve() error { case <-l.ctx.Done(): return trace.Wrap(net.ErrClosed, "context is closing") case <-time.After(5. * time.Second): - l.Debugf("Backoff on network error.") + l.logger.DebugContext(l.ctx, "Backoff on network error") } } else { go l.forwardConnection(conn) @@ -242,7 +243,7 @@ func (l *LoadBalancer) Serve() error { func (l *LoadBalancer) forwardConnection(conn net.Conn) { err := l.forward(conn) if err != nil { - l.Warningf("Failed to forward connection: %v.", err) + l.logger.WarnContext(l.ctx, "Failed to forward connection", "error", err) } } @@ -278,11 +279,11 @@ func (l *LoadBalancer) forward(conn net.Conn) error { backendConnID := l.trackConnection(backend, backendConn) defer l.untrackConnection(backend, backendConnID) - logger := l.WithFields(log.Fields{ - "source": conn.RemoteAddr(), - "dest": backendConn.RemoteAddr(), - }) - logger.Debugf("forward") + logger := l.logger.With( + "source_addr", logutils.StringerAttr(conn.RemoteAddr()), + "dest_addr", logutils.StringerAttr(backendConn.RemoteAddr()), + ) + logger.DebugContext(l.ctx, "forwarding data") messagesC := make(chan error, 2) @@ -305,7 +306,7 @@ func (l *LoadBalancer) forward(conn net.Conn) error { select { case err := <-messagesC: if err != nil && !errors.Is(err, io.EOF) { - logger.Warningf("connection problem: %v %T", trace.DebugReport(err), err) + logger.WarnContext(l.ctx, "connection problem", "error", err) lastErr = err } case <-l.ctx.Done(): diff --git a/lib/utils/proxy/proxy.go b/lib/utils/proxy/proxy.go index c34356dd981a5..f9995b0c12a92 100644 --- a/lib/utils/proxy/proxy.go +++ b/lib/utils/proxy/proxy.go @@ -25,18 +25,16 @@ import ( "time" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" apiclient "github.com/gravitational/teleport/api/client" tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh" apiutils "github.com/gravitational/teleport/api/utils" + logutils "github.com/gravitational/teleport/lib/utils/log" ) -var log = logrus.WithFields(logrus.Fields{ - teleport.ComponentKey: teleport.ComponentConnectProxy, -}) +var log = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentConnectProxy) // A Dialer is a means for a client to establish a SSH connection. type Dialer interface { @@ -194,13 +192,15 @@ func DialerFromEnvironment(addr string, opts ...DialerOptionFunc) Dialer { // If no proxy settings are in environment return regular ssh dialer, // otherwise return a proxy dialer. if proxyURL == nil { - log.Debugf("No proxy set in environment, returning direct dialer.") + log.DebugContext(context.Background(), "No proxy set in environment, returning direct dialer") return directDial{ alpnDialer: options.alpnDialer, proxyHeaderGetter: options.proxyHeaderGetter, } } - log.Debugf("Found proxy %q in environment, returning proxy dialer.", proxyURL) + log.DebugContext(context.Background(), "Found proxy in environment, returning proxy dialer", + "proxy_url", logutils.StringerAttr(proxyURL), + ) return proxyDial{ proxyURL: proxyURL, insecure: options.insecureSkipTLSVerify, diff --git a/lib/utils/proxyconn_test.go b/lib/utils/proxyconn_test.go index cf7a8a82faf35..0eae440a4f72b 100644 --- a/lib/utils/proxyconn_test.go +++ b/lib/utils/proxyconn_test.go @@ -22,6 +22,7 @@ import ( "context" "fmt" "io" + "log/slog" "net" "strings" "testing" @@ -29,7 +30,6 @@ import ( "github.com/google/uuid" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/gravitational/teleport" @@ -115,7 +115,7 @@ func TestProxyConnCancel(t *testing.T) { type echoServer struct { listener net.Listener - log logrus.FieldLogger + log *slog.Logger } func newEchoServer() (*echoServer, error) { @@ -125,7 +125,7 @@ func newEchoServer() (*echoServer, error) { } return &echoServer{ listener: listener, - log: logrus.WithField(teleport.ComponentKey, "echo"), + log: slog.With(teleport.ComponentKey, "echo"), }, nil } @@ -154,7 +154,7 @@ func (s *echoServer) handleConn(conn net.Conn) error { if err != nil { return trace.Wrap(err) } - s.log.Infof("Received message: %s.", b) + s.log.InfoContext(context.Background(), "Received message", "receieved_message", string(b)) _, err = conn.Write(b) if err != nil { diff --git a/lib/utils/registry/registry_windows.go b/lib/utils/registry/registry_windows.go index 9b41f3b505fbf..5b254bc127ae8 100644 --- a/lib/utils/registry/registry_windows.go +++ b/lib/utils/registry/registry_windows.go @@ -19,12 +19,13 @@ package registry import ( + "context" "errors" + "log/slog" "os" "strconv" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "golang.org/x/sys/windows/registry" ) @@ -34,14 +35,17 @@ func GetOrCreateRegistryKey(name string) (registry.Key, error) { reg, err := registry.OpenKey(registry.CURRENT_USER, name, registry.QUERY_VALUE|registry.CREATE_SUB_KEY|registry.SET_VALUE) switch { case errors.Is(err, os.ErrNotExist): - log.Debugf("Registry key %v doesn't exist, trying to create it", name) + slog.DebugContext(context.Background(), "Registry key doesn't exist, trying to create it", "key_name", name) reg, _, err = registry.CreateKey(registry.CURRENT_USER, name, registry.QUERY_VALUE|registry.CREATE_SUB_KEY|registry.SET_VALUE) if err != nil { - log.Debugf("Can't create registry key %v: %v", name, err) + slog.DebugContext(context.Background(), "Can't create registry key", + "key_name", name, + "error", err, + ) return reg, err } case err != nil: - log.Errorf("registry.OpenKey returned error: %v", err) + slog.ErrorContext(context.Background(), "registry.OpenKey returned error", "error", err) return reg, err default: return reg, nil @@ -53,12 +57,20 @@ func GetOrCreateRegistryKey(name string) (registry.Key, error) { func WriteDword(k registry.Key, name string, value string) error { dwordValue, err := strconv.ParseUint(value, 10, 32) if err != nil { - log.Debugf("Failed to convert value %v to uint32: %v", value, err) + slog.DebugContext(context.Background(), "Failed to convert value to uint32", + "value", value, + "error", err, + ) return trace.Wrap(err) } err = k.SetDWordValue(name, uint32(dwordValue)) if err != nil { - log.Debugf("Failed to write dword %v: %v to registry key %v: %v", name, value, k, err) + slog.DebugContext(context.Background(), "Failed to write dword to registry key", + "name", name, + "value", value, + "key_name", k, + "error", err, + ) return trace.Wrap(err) } return nil @@ -68,7 +80,12 @@ func WriteDword(k registry.Key, name string, value string) error { func WriteString(k registry.Key, name string, value string) error { err := k.SetStringValue(name, value) if err != nil { - log.Debugf("Failed to write string %v: %v to registry key %v: %v", name, value, k, err) + slog.DebugContext(context.Background(), "Failed to write string to registry key", + "name", name, + "value", value, + "key_name", k, + "error", err, + ) return trace.Wrap(err) } return nil @@ -78,7 +95,12 @@ func WriteString(k registry.Key, name string, value string) error { func WriteMultiString(k registry.Key, name string, values []string) error { err := k.SetStringsValue(name, values) if err != nil { - log.Debugf("Failed to write strings %v: %v to registry key %v: %v", name, values, k, err) + slog.DebugContext(context.Background(), "Failed to write strings to registry key", + "name", name, + "values", values, + "key_name", k, + "error", err, + ) return trace.Wrap(err) } return nil diff --git a/lib/utils/socks/socks.go b/lib/utils/socks/socks.go index 374c1a7c41659..afcd5bdacd424 100644 --- a/lib/utils/socks/socks.go +++ b/lib/utils/socks/socks.go @@ -27,15 +27,8 @@ import ( "strconv" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" - - "github.com/gravitational/teleport" ) -var log = logrus.WithFields(logrus.Fields{ - teleport.ComponentKey: teleport.ComponentSOCKS, -}) - const ( socks5Version byte = 0x05 socks5Reserved byte = 0x00 diff --git a/lib/utils/socks/socks_test.go b/lib/utils/socks/socks_test.go index 4babebc5f8882..0dcc2ddcb44c1 100644 --- a/lib/utils/socks/socks_test.go +++ b/lib/utils/socks/socks_test.go @@ -19,7 +19,9 @@ package socks import ( + "context" "io" + "log/slog" "net" "os" "testing" @@ -99,7 +101,7 @@ func (d *debugServer) Serve() { for { conn, err := d.ln.Accept() if err != nil { - log.Debugf("Failed to accept connection: %v.", err) + slog.DebugContext(context.Background(), "Failed to accept connection", "error", err) break } @@ -114,17 +116,17 @@ func (d *debugServer) handle(conn net.Conn) { remoteAddr, err := Handshake(conn) if err != nil { - log.Debugf("Handshake failed: %v.", err) + slog.DebugContext(context.Background(), "Handshake failed", "error", err) return } n, err := conn.Write([]byte(remoteAddr)) if err != nil { - log.Debugf("Failed to write to connection: %v.", err) + slog.DebugContext(context.Background(), "Failed to write to connection", "error", err) return } if n != len(remoteAddr) { - log.Debugf("Short write, wrote %v wanted %v.", n, len(remoteAddr)) + slog.DebugContext(context.Background(), "Short write", "wrote", n, "wanted", len(remoteAddr)) return } } diff --git a/lib/utils/typical/cached_parser.go b/lib/utils/typical/cached_parser.go index 5a60d3724fc35..27b61defd31bf 100644 --- a/lib/utils/typical/cached_parser.go +++ b/lib/utils/typical/cached_parser.go @@ -19,13 +19,14 @@ package typical import ( + "context" + "log/slog" "os" "strconv" "sync/atomic" "github.com/gravitational/trace" lru "github.com/hashicorp/golang-lru/v2" - "github.com/sirupsen/logrus" ) const ( @@ -56,7 +57,7 @@ type CachedParser[TEnv, TResult any] struct { Parser[TEnv, TResult] cache *lru.Cache[string, Expression[TEnv, TResult]] evictedCount atomic.Uint32 - logger logger + logger *slog.Logger } // NewCachedParser creates a cached predicate expression parser with the given specification. @@ -72,7 +73,7 @@ func NewCachedParser[TEnv, TResult any](spec ParserSpec[TEnv], opts ...ParserOpt return &CachedParser[TEnv, TResult]{ Parser: *parser, cache: cache, - logger: logrus.StandardLogger(), + logger: slog.Default(), }, nil } @@ -88,12 +89,9 @@ func (c *CachedParser[TEnv, TResult]) Parse(expression string) (Expression[TEnv, return nil, trace.Wrap(err) } if evicted := c.cache.Add(expression, parsedExpr); evicted && c.evictedCount.Add(1)%logAfterEvictions == 0 { - c.logger.Infof("%d entries have been evicted from the predicate expression cache, consider increasing TELEPORT_EXPRESSION_CACHE_SIZE", - logAfterEvictions) + c.logger.InfoContext(context.Background(), "entries have been evicted from the predicate expression cache, consider increasing TELEPORT_EXPRESSION_CACHE_SIZE", + "eviticiton_count", logAfterEvictions, + ) } return parsedExpr, nil } - -type logger interface { - Infof(fmt string, args ...any) -} diff --git a/lib/utils/typical/cached_parser_test.go b/lib/utils/typical/cached_parser_test.go index 1bfb67bd7f3c3..1f15207ab3f68 100644 --- a/lib/utils/typical/cached_parser_test.go +++ b/lib/utils/typical/cached_parser_test.go @@ -25,14 +25,6 @@ import ( "github.com/stretchr/testify/require" ) -type testLogger struct { - loggedCount int -} - -func (t *testLogger) Infof(msg string, args ...any) { - t.loggedCount++ -} - func TestCachedParser(t *testing.T) { // A simple cached parser with no environment that always returns an int. p, err := NewCachedParser[struct{}, int](ParserSpec[struct{}]{ @@ -45,9 +37,6 @@ func TestCachedParser(t *testing.T) { require.NoError(t, err) require.NotNil(t, p) - var logger testLogger - p.logger = &logger - // Sanity check we still get errors. _, err = p.Parse(`"hello"`) require.Error(t, err) @@ -93,9 +82,6 @@ func TestCachedParser(t *testing.T) { // Check that each new parsed expression results in an eviction. require.Equal(t, uint32(i+1), p.evictedCount.Load()) - - // Check that no log was printed - require.Equal(t, 0, logger.loggedCount) } // Parse one more expression @@ -103,9 +89,6 @@ func TestCachedParser(t *testing.T) { _, err = p.Parse(expr) require.NoError(t, err) - // Check a log was printed once after $logAfterEvictions evictions. - require.Equal(t, 1, logger.loggedCount) - // Parse another $logAfterEvictions unique expressions to cause // another $logAfterEvictions cache evictions and one more log for i := 0; i < logAfterEvictions; i++ { @@ -113,7 +96,4 @@ func TestCachedParser(t *testing.T) { _, err := p.Parse(expr) require.NoError(t, err) } - - // Check a log was printed twice after 2*$logAfterEvictions evictions. - require.Equal(t, 2, logger.loggedCount) } diff --git a/lib/utils/unpack.go b/lib/utils/unpack.go index 14b213f08a173..e42d38283eba2 100644 --- a/lib/utils/unpack.go +++ b/lib/utils/unpack.go @@ -20,15 +20,16 @@ package utils import ( "archive/tar" + "context" "errors" "io" + "log/slog" "os" "path" "path/filepath" "strings" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "github.com/gravitational/teleport" ) @@ -140,7 +141,10 @@ func extractFile(tarball *tar.Reader, header *tar.Header, dir string, dirMode os case tar.TypeSymlink: return writeSymbolicLink(filepath.Join(dir, header.Name), header.Linkname, dirMode) default: - log.Warnf("Unsupported type flag %v for %v.", header.Typeflag, header.Name) + slog.WarnContext(context.Background(), "Unsupported type flag for taball", + "type_flag", header.Typeflag, + "header", header.Name, + ) } return nil } diff --git a/lib/utils/utils.go b/lib/utils/utils.go index 1990f39ad23e2..4008b452ba6a6 100644 --- a/lib/utils/utils.go +++ b/lib/utils/utils.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "io/fs" + "log/slog" "math/rand/v2" "net" "net/url" @@ -38,7 +39,6 @@ import ( "unicode" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/util/validation" "github.com/gravitational/teleport" @@ -124,13 +124,17 @@ func NewTracer(description string) *Tracer { // Start logs start of the trace func (t *Tracer) Start() *Tracer { - log.Debugf("Tracer started %v.", t.Description) + slog.DebugContext(context.Background(), "Tracer started", + "trace", t.Description) return t } // Stop logs stop of the trace func (t *Tracer) Stop() *Tracer { - log.Debugf("Tracer completed %v in %v.", t.Description, time.Since(t.Started)) + slog.DebugContext(context.Background(), "Tracer completed", + "trace", t.Description, + "duration", time.Since(t.Started), + ) return t } From a9d27e1d0264a1c4bc3b9927d7125414246cc9e5 Mon Sep 17 00:00:00 2001 From: Zac Bergquist Date: Thu, 19 Dec 2024 11:07:36 -0700 Subject: [PATCH 23/64] Fix web session playback when a duration is not provided (#50262) Prior to Teleport 15, the web UI would download the entire session recording into browser memory before playing it back (crashing the browser tab for long sessions). Starting with Teleport 15, session playback is streamed to the browser and played back as it is received instead of waiting for the entire session to be available. A side effect of this change is that the browser needs to know the length of the session in order to render the progress bar during playback. Since the browser starts playing the session before it has received all of it, we started providing the length of the session via a URL query parameter. Some users have grown accustomed to being able to access session recordings at their original URLs (without the duration query parameter). If you attempt to play recordings from these URLs after upgrading to v15, you'll get an error that the duration is missing. To fix this, the web UI needs to request the duration of the session before it can begin playing it (unless the duration is provided via the URL). There are two ways we could get this information: 1. By querying Teleport's audit log 2. By reading the recording twice: once to get to the end event and compute the duration, and a second time to actually play it back. Since we only have a session ID, an audit log query would be inefficient - we have no idea when the session occurred, so we'd have to search from the beginning of time. (This could be resolved by using a UUIDv7 for session IDs, but Teleport uses UUIDv4 today). For this reason, we elect option 2. This commit creates a new web API endpoint that will fetch a session recording file and scan through it in the same way that streaming is done, but instaed of streaming the data through a websocket it simply reads through to the end to compute the session length. The benefit of this approach is that it will generally be faster than option 1 (unless the session is very long), and it effectively pre-downloads the recording file on the Note: option 2 is not without its drawbacks - since the web UI is making two requests that both read the session recording, the audit log will show two separate session_recording.access events. This isn't ideal but it is good enough to get playback working again for customers who don't access playbacks by clicking the "Play" button in the UI. --- lib/client/api.go | 13 ---- lib/client/api_test.go | 75 ------------------- lib/events/auditlog.go | 1 + lib/web/apiserver.go | 1 + lib/web/tty_playback.go | 41 ++++++++++ web/packages/teleport/src/Player/Player.tsx | 68 ++++++++++++++--- web/packages/teleport/src/config.ts | 23 ++++-- .../teleport/src/services/mfa/types.ts | 3 +- .../src/services/recordings/recordings.ts | 7 ++ 9 files changed, 122 insertions(+), 110 deletions(-) diff --git a/lib/client/api.go b/lib/client/api.go index 88693bc768bb4..6cc0caae15286 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -5032,19 +5032,6 @@ func findActiveApps(keyRing *KeyRing) ([]tlsca.RouteToApp, error) { return apps, nil } -// getDesktopEventWebURL returns the web UI URL users can access to -// watch a desktop session recording in the browser -func getDesktopEventWebURL(proxyHost string, cluster string, sid *session.ID, events []events.EventFields) string { - if len(events) < 1 { - return "" - } - start := events[0].GetTimestamp() - end := events[len(events)-1].GetTimestamp() - duration := end.Sub(start) - - return fmt.Sprintf("https://%s/web/cluster/%s/session/%s?recordingType=desktop&durationMs=%d", proxyHost, cluster, sid, duration/time.Millisecond) -} - // SearchSessionEvents allows searching for session events with a full pagination support. func (tc *TeleportClient) SearchSessionEvents(ctx context.Context, fromUTC, toUTC time.Time, pageSize int, order types.EventOrder, max int) ([]apievents.AuditEvent, error) { ctx, span := tc.Tracer.Start( diff --git a/lib/client/api_test.go b/lib/client/api_test.go index 9da016c7af401..136d338b01e39 100644 --- a/lib/client/api_test.go +++ b/lib/client/api_test.go @@ -27,7 +27,6 @@ import ( "math" "os" "testing" - "time" "github.com/coreos/go-semver/semver" "github.com/google/go-cmp/cmp" @@ -45,10 +44,8 @@ import ( "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" - "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/observability/tracing" - "github.com/gravitational/teleport/lib/session" "github.com/gravitational/teleport/lib/utils" ) @@ -929,78 +926,6 @@ func TestFormatConnectToProxyErr(t *testing.T) { } } -func TestGetDesktopEventWebURL(t *testing.T) { - initDate := time.Date(2021, 1, 1, 12, 0, 0, 0, time.UTC) - - tt := []struct { - name string - proxyHost string - cluster string - sid session.ID - events []events.EventFields - expected string - }{ - { - name: "nil events", - events: nil, - expected: "", - }, - { - name: "empty events", - events: make([]events.EventFields, 0), - expected: "", - }, - { - name: "two events, 1000 ms duration", - proxyHost: "host", - cluster: "cluster", - sid: "session_id", - events: []events.EventFields{ - { - "time": initDate, - }, - { - "time": initDate.Add(1000 * time.Millisecond), - }, - }, - expected: "https://host/web/cluster/cluster/session/session_id?recordingType=desktop&durationMs=1000", - }, - { - name: "multiple events", - proxyHost: "host", - cluster: "cluster", - sid: "session_id", - events: []events.EventFields{ - { - "time": initDate, - }, - { - "time": initDate.Add(10 * time.Millisecond), - }, - { - "time": initDate.Add(20 * time.Millisecond), - }, - { - "time": initDate.Add(30 * time.Millisecond), - }, - { - "time": initDate.Add(40 * time.Millisecond), - }, - { - "time": initDate.Add(50 * time.Millisecond), - }, - }, - expected: "https://host/web/cluster/cluster/session/session_id?recordingType=desktop&durationMs=50", - }, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - require.Equal(t, tc.expected, getDesktopEventWebURL(tc.proxyHost, tc.cluster, &tc.sid, tc.events)) - }) - } -} - type mockRoleGetter func(ctx context.Context) ([]types.Role, error) func (m mockRoleGetter) GetRoles(ctx context.Context) ([]types.Role, error) { diff --git a/lib/events/auditlog.go b/lib/events/auditlog.go index 51180746cbe7f..3570171f40996 100644 --- a/lib/events/auditlog.go +++ b/lib/events/auditlog.go @@ -555,6 +555,7 @@ func (l *AuditLog) StreamSessionEvents(ctx context.Context, sessionID session.ID } protoReader := NewProtoReader(rawSession) + defer protoReader.Close() for { if ctx.Err() != nil { diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index b88f4c0102edf..597360e64df8a 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -844,6 +844,7 @@ func (h *Handler) bindDefaultEndpoints() { h.GET("/webapi/sites/:site/events/search", h.WithClusterAuth(h.clusterSearchEvents)) // search site events h.GET("/webapi/sites/:site/events/search/sessions", h.WithClusterAuth(h.clusterSearchSessionEvents)) // search site session events h.GET("/webapi/sites/:site/ttyplayback/:sid", h.WithClusterAuth(h.ttyPlaybackHandle)) + h.GET("/webapi/sites/:site/sessionlength/:sid", h.WithClusterAuth(h.sessionLengthHandle)) // scp file transfer h.GET("/webapi/sites/:site/nodes/:server/:login/scp", h.WithClusterAuth(h.transferFile)) diff --git a/lib/web/tty_playback.go b/lib/web/tty_playback.go index f601f4237666c..76c456a4a7010 100644 --- a/lib/web/tty_playback.go +++ b/lib/web/tty_playback.go @@ -53,6 +53,47 @@ const ( actionPause = byte(1) ) +func (h *Handler) sessionLengthHandle( + w http.ResponseWriter, + r *http.Request, + p httprouter.Params, + sctx *SessionContext, + site reversetunnelclient.RemoteSite, +) (interface{}, error) { + sID := p.ByName("sid") + if sID == "" { + return nil, trace.BadParameter("missing session ID in request URL") + } + + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + + clt, err := sctx.GetUserClient(ctx, site) + if err != nil { + return nil, trace.Wrap(err) + } + + evts, errs := clt.StreamSessionEvents(ctx, session.ID(sID), 0) + for { + select { + case err := <-errs: + return nil, trace.Wrap(err) + case evt, ok := <-evts: + if !ok { + return nil, trace.NotFound("could not find end event for session %v", sID) + } + switch evt := evt.(type) { + case *events.SessionEnd: + return map[string]any{"durationMs": evt.EndTime.Sub(evt.StartTime).Milliseconds()}, nil + case *events.WindowsDesktopSessionEnd: + return map[string]any{"durationMs": evt.EndTime.Sub(evt.StartTime).Milliseconds()}, nil + case *events.DatabaseSessionEnd: + return map[string]any{"durationMs": evt.EndTime.Sub(evt.StartTime).Milliseconds()}, nil + } + } + } +} + func (h *Handler) ttyPlaybackHandle( w http.ResponseWriter, r *http.Request, diff --git a/web/packages/teleport/src/Player/Player.tsx b/web/packages/teleport/src/Player/Player.tsx index 190f8afb1b558..7d26a785d1830 100644 --- a/web/packages/teleport/src/Player/Player.tsx +++ b/web/packages/teleport/src/Player/Player.tsx @@ -16,20 +16,22 @@ * along with this program. If not, see . */ +import { useCallback, useEffect } from 'react'; import styled from 'styled-components'; -import { Flex, Box } from 'design'; - +import { Flex, Box, Indicator } from 'design'; import { Danger } from 'design/Alert'; -import { useParams, useLocation } from 'teleport/components/Router'; +import { makeSuccessAttempt, useAsync } from 'shared/hooks/useAsync'; +import { useParams, useLocation } from 'teleport/components/Router'; import session from 'teleport/services/websession'; import { UrlPlayerParams } from 'teleport/config'; import { getUrlParameter } from 'teleport/services/history'; - import { RecordingType } from 'teleport/services/recordings'; +import useTeleport from 'teleport/useTeleport'; + import ActionBar from './ActionBar'; import { DesktopPlayer } from './DesktopPlayer'; import SshPlayer from './SshPlayer'; @@ -38,19 +40,44 @@ import Tabs, { TabItem } from './PlayerTabs'; const validRecordingTypes = ['ssh', 'k8s', 'desktop', 'database']; export function Player() { + const ctx = useTeleport(); const { sid, clusterId } = useParams(); const { search } = useLocation(); + useEffect(() => { + document.title = `Play ${sid} • ${clusterId}`; + }, [sid, clusterId]); + const recordingType = getUrlParameter( 'recordingType', search ) as RecordingType; - const durationMs = Number(getUrlParameter('durationMs', search)); + + // In order to render the progress bar, we need to know the length of the session. + // All in-product links to the session player should include the session duration in the URL. + // Some users manually build the URL based on the session ID and don't specify the session duration. + // For those cases, we make a separate API call to get the duration. + const [fetchDurationAttempt, fetchDuration] = useAsync( + useCallback( + () => ctx.recordingsService.fetchRecordingDuration(clusterId, sid), + [ctx.recordingsService, clusterId, sid] + ) + ); const validRecordingType = validRecordingTypes.includes(recordingType); - const validDurationMs = Number.isInteger(durationMs) && durationMs > 0; + const durationMs = Number(getUrlParameter('durationMs', search)); + const shouldFetchSessionDuration = + validRecordingType && (!Number.isInteger(durationMs) || durationMs <= 0); + + useEffect(() => { + if (shouldFetchSessionDuration) { + fetchDuration(); + } + }, [fetchDuration, shouldFetchSessionDuration]); - document.title = `Play ${sid} • ${clusterId}`; + const combinedAttempt = shouldFetchSessionDuration + ? fetchDurationAttempt + : makeSuccessAttempt({ durationMs }); function onLogout() { session.logout(); @@ -69,13 +96,25 @@ export function Player() { ); } - if (!validDurationMs) { + if ( + combinedAttempt.status === '' || + combinedAttempt.status === 'processing' + ) { + return ( + + + + + + ); + } + if (combinedAttempt.status === 'error') { return ( - Invalid query parameter durationMs:{' '} - {getUrlParameter('durationMs', search)}, should be an integer. + Unable to determine the length of this session. The session + recording may be incomplete or corrupted. @@ -101,15 +140,20 @@ export function Player() { ) : ( - + )} ); } + const StyledPlayer = styled.div` display: flex; height: 100%; diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index 109da510a3ac0..f69a468f121fe 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -17,19 +17,21 @@ */ import { generatePath } from 'react-router'; -import { mergeDeep } from 'shared/utils/highbar'; -import { IncludedResourceMode } from 'shared/components/UnifiedResources'; -import generateResourcePath from './generateResourcePath'; +import { IncludedResourceMode } from 'shared/components/UnifiedResources'; -import { defaultEntitlements } from './entitlement'; +import { mergeDeep } from 'shared/utils/highbar'; import { AwsOidcPolicyPreset, IntegrationKind, PluginKind, Regions, -} from './services/integrations'; +} from 'teleport/services/integrations'; + +import { defaultEntitlements } from './entitlement'; + +import generateResourcePath from './generateResourcePath'; import type { Auth2faType, @@ -40,11 +42,11 @@ import type { } from 'shared/services'; import type { SortType } from 'teleport/services/agents'; +import type { KubeResourceKind } from 'teleport/services/kube/types'; +import type { WebauthnAssertionResponse } from 'teleport/services/mfa'; import type { RecordingType } from 'teleport/services/recordings'; -import type { WebauthnAssertionResponse } from './services/mfa'; import type { ParticipantMode } from 'teleport/services/session'; -import type { YamlSupportedResourceKind } from './services/yaml/types'; -import type { KubeResourceKind } from './services/kube/types'; +import type { YamlSupportedResourceKind } from 'teleport/services/yaml/types'; const cfg = { /** @deprecated Use cfg.edition instead. */ @@ -264,6 +266,7 @@ const cfg = { ttyPlaybackWsAddr: 'wss://:fqdn/v1/webapi/sites/:clusterId/ttyplayback/:sid?access_token=:token', // TODO(zmb3): get token out of URL activeAndPendingSessionsPath: '/v1/webapi/sites/:clusterId/sessions', + sessionDurationPath: '/v1/webapi/sites/:clusterId/sessionlength/:sid', kubernetesPath: '/v1/webapi/sites/:clusterId/kubernetes?searchAsRoles=:searchAsRoles?&limit=:limit?&startKey=:startKey?&query=:query?&search=:search?&sort=:sort?', @@ -774,6 +777,10 @@ const cfg = { return generatePath(cfg.api.activeAndPendingSessionsPath, { clusterId }); }, + getSessionDurationUrl(clusterId: string, sid: string) { + return generatePath(cfg.api.sessionDurationPath, { clusterId, sid }); + }, + getUnifiedResourcesUrl(clusterId: string, params: UrlResourcesParams) { return generateResourcePath(cfg.api.unifiedResourcesPath, { clusterId, diff --git a/web/packages/teleport/src/services/mfa/types.ts b/web/packages/teleport/src/services/mfa/types.ts index f1292c50c99cd..382d7831f82fe 100644 --- a/web/packages/teleport/src/services/mfa/types.ts +++ b/web/packages/teleport/src/services/mfa/types.ts @@ -18,8 +18,7 @@ import { AuthProviderType } from 'shared/services'; -import { Base64urlString } from '../auth/types'; -import { CreateNewHardwareDeviceRequest } from '../auth/types'; +import { Base64urlString, CreateNewHardwareDeviceRequest } from '../auth/types'; export type DeviceType = 'totp' | 'webauthn' | 'sso'; diff --git a/web/packages/teleport/src/services/recordings/recordings.ts b/web/packages/teleport/src/services/recordings/recordings.ts index e27ca67beea03..ba71160aa1795 100644 --- a/web/packages/teleport/src/services/recordings/recordings.ts +++ b/web/packages/teleport/src/services/recordings/recordings.ts @@ -45,4 +45,11 @@ export default class RecordingsService { return { recordings: events.map(makeRecording), startKey: json.startKey }; }); } + + fetchRecordingDuration( + clusterId: string, + sessionId: string + ): Promise<{ durationMs: number }> { + return api.get(cfg.getSessionDurationUrl(clusterId, sessionId)); + } } From 860a9d4c3346d89376a2f6e675ede593d43595f0 Mon Sep 17 00:00:00 2001 From: Alan Parra Date: Thu, 19 Dec 2024 15:38:35 -0300 Subject: [PATCH 24/64] Add conversions to/from decisionpb.TLSIdentity (#50308) * Add conversions to/from decisionpb.TLSIdentity * Map timestamppb.Timestamp{} to 0-0-0 0:0.0 (instead of unix epoch) * Document that slices are not deep-copied --- lib/decision/tls_identity.go | 278 ++++++++++++++++++++++++++++++ lib/decision/tls_identity_test.go | 171 ++++++++++++++++++ 2 files changed, 449 insertions(+) create mode 100644 lib/decision/tls_identity.go create mode 100644 lib/decision/tls_identity_test.go diff --git a/lib/decision/tls_identity.go b/lib/decision/tls_identity.go new file mode 100644 index 0000000000000..d0cf1c7905eab --- /dev/null +++ b/lib/decision/tls_identity.go @@ -0,0 +1,278 @@ +// 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 decision + +import ( + "time" + + "google.golang.org/protobuf/types/known/timestamppb" + + decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1" + traitpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trait/v1" + "github.com/gravitational/teleport/api/types" + apitrait "github.com/gravitational/teleport/api/types/trait" + apitraitconvert "github.com/gravitational/teleport/api/types/trait/convert/v1" + "github.com/gravitational/teleport/api/types/wrappers" + "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/lib/tlsca" +) + +// TLSIdentityToTLSCA transforms a [decisionpb.TLSIdentity] into its +// equivalent [tlsca.Identity]. +// Note that certain types, like slices, are not deep-copied. +func TLSIdentityToTLSCA(id *decisionpb.TLSIdentity) *tlsca.Identity { + if id == nil { + return nil + } + + return &tlsca.Identity{ + Username: id.Username, + Impersonator: id.Impersonator, + Groups: id.Groups, + SystemRoles: id.SystemRoles, + Usage: id.Usage, + Principals: id.Principals, + KubernetesGroups: id.KubernetesGroups, + KubernetesUsers: id.KubernetesUsers, + Expires: timestampToGoTime(id.Expires), + RouteToCluster: id.RouteToCluster, + KubernetesCluster: id.KubernetesCluster, + Traits: traitToWrappers(id.Traits), + RouteToApp: routeToAppFromProto(id.RouteToApp), + TeleportCluster: id.TeleportCluster, + RouteToDatabase: routeToDatabaseFromProto(id.RouteToDatabase), + DatabaseNames: id.DatabaseNames, + DatabaseUsers: id.DatabaseUsers, + MFAVerified: id.MfaVerified, + PreviousIdentityExpires: timestampToGoTime(id.PreviousIdentityExpires), + LoginIP: id.LoginIp, + PinnedIP: id.PinnedIp, + AWSRoleARNs: id.AwsRoleArns, + AzureIdentities: id.AzureIdentities, + GCPServiceAccounts: id.GcpServiceAccounts, + ActiveRequests: id.ActiveRequests, + DisallowReissue: id.DisallowReissue, + Renewable: id.Renewable, + Generation: id.Generation, + BotName: id.BotName, + BotInstanceID: id.BotInstanceId, + AllowedResourceIDs: resourceIDsToTypes(id.AllowedResourceIds), + PrivateKeyPolicy: keys.PrivateKeyPolicy(id.PrivateKeyPolicy), + ConnectionDiagnosticID: id.ConnectionDiagnosticId, + DeviceExtensions: deviceExtensionsFromProto(id.DeviceExtensions), + UserType: types.UserType(id.UserType), + } +} + +// TLSIdentityFromTLSCA transforms a [tlsca.Identity] into its equivalent +// [decisionpb.TLSIdentity]. +// Note that certain types, like slices, are not deep-copied. +func TLSIdentityFromTLSCA(id *tlsca.Identity) *decisionpb.TLSIdentity { + if id == nil { + return nil + } + + return &decisionpb.TLSIdentity{ + Username: id.Username, + Impersonator: id.Impersonator, + Groups: id.Groups, + SystemRoles: id.SystemRoles, + Usage: id.Usage, + Principals: id.Principals, + KubernetesGroups: id.KubernetesGroups, + KubernetesUsers: id.KubernetesUsers, + Expires: timestampFromGoTime(id.Expires), + RouteToCluster: id.RouteToCluster, + KubernetesCluster: id.KubernetesCluster, + Traits: traitFromWrappers(id.Traits), + RouteToApp: routeToAppToProto(&id.RouteToApp), + TeleportCluster: id.TeleportCluster, + RouteToDatabase: routeToDatabaseToProto(&id.RouteToDatabase), + DatabaseNames: id.DatabaseNames, + DatabaseUsers: id.DatabaseUsers, + MfaVerified: id.MFAVerified, + PreviousIdentityExpires: timestampFromGoTime(id.PreviousIdentityExpires), + LoginIp: id.LoginIP, + PinnedIp: id.PinnedIP, + AwsRoleArns: id.AWSRoleARNs, + AzureIdentities: id.AzureIdentities, + GcpServiceAccounts: id.GCPServiceAccounts, + ActiveRequests: id.ActiveRequests, + DisallowReissue: id.DisallowReissue, + Renewable: id.Renewable, + Generation: id.Generation, + BotName: id.BotName, + BotInstanceId: id.BotInstanceID, + AllowedResourceIds: resourceIDsFromTypes(id.AllowedResourceIDs), + PrivateKeyPolicy: string(id.PrivateKeyPolicy), + ConnectionDiagnosticId: id.ConnectionDiagnosticID, + DeviceExtensions: deviceExtensionsToProto(&id.DeviceExtensions), + UserType: string(id.UserType), + } +} + +func timestampToGoTime(t *timestamppb.Timestamp) time.Time { + // nil or "zero" Timestamps are mapped to Go's zero time (0-0-0 0:0.0) instead + // of unix epoch. The latter avoids problems with tooling (eg, Terraform) that + // sets structs to their defaults instead of using nil. + if t == nil || (t.Seconds == 0 && t.Nanos == 0) { + return time.Time{} + } + return t.AsTime() +} + +func timestampFromGoTime(t time.Time) *timestamppb.Timestamp { + if t.IsZero() { + return nil + } + return timestamppb.New(t) +} + +func traitToWrappers(traits []*traitpb.Trait) wrappers.Traits { + apiTraits := apitraitconvert.FromProto(traits) + return wrappers.Traits(apiTraits) +} + +func traitFromWrappers(traits wrappers.Traits) []*traitpb.Trait { + if len(traits) == 0 { + return nil + } + apiTraits := apitrait.Traits(traits) + return apitraitconvert.ToProto(apiTraits) +} + +func routeToAppFromProto(routeToApp *decisionpb.RouteToApp) tlsca.RouteToApp { + if routeToApp == nil { + return tlsca.RouteToApp{} + } + + return tlsca.RouteToApp{ + SessionID: routeToApp.SessionId, + PublicAddr: routeToApp.PublicAddr, + ClusterName: routeToApp.ClusterName, + Name: routeToApp.Name, + AWSRoleARN: routeToApp.AwsRoleArn, + AzureIdentity: routeToApp.AzureIdentity, + GCPServiceAccount: routeToApp.GcpServiceAccount, + URI: routeToApp.Uri, + TargetPort: int(routeToApp.TargetPort), + } +} + +func routeToAppToProto(routeToApp *tlsca.RouteToApp) *decisionpb.RouteToApp { + if routeToApp == nil { + return nil + } + + return &decisionpb.RouteToApp{ + SessionId: routeToApp.SessionID, + PublicAddr: routeToApp.PublicAddr, + ClusterName: routeToApp.ClusterName, + Name: routeToApp.Name, + AwsRoleArn: routeToApp.AWSRoleARN, + AzureIdentity: routeToApp.AzureIdentity, + GcpServiceAccount: routeToApp.GCPServiceAccount, + Uri: routeToApp.URI, + TargetPort: int32(routeToApp.TargetPort), + } +} + +func routeToDatabaseFromProto(routeToDatabase *decisionpb.RouteToDatabase) tlsca.RouteToDatabase { + if routeToDatabase == nil { + return tlsca.RouteToDatabase{} + } + + return tlsca.RouteToDatabase{ + ServiceName: routeToDatabase.ServiceName, + Protocol: routeToDatabase.Protocol, + Username: routeToDatabase.Username, + Database: routeToDatabase.Database, + Roles: routeToDatabase.Roles, + } +} + +func routeToDatabaseToProto(routeToDatabase *tlsca.RouteToDatabase) *decisionpb.RouteToDatabase { + if routeToDatabase == nil { + return nil + } + + return &decisionpb.RouteToDatabase{ + ServiceName: routeToDatabase.ServiceName, + Protocol: routeToDatabase.Protocol, + Username: routeToDatabase.Username, + Database: routeToDatabase.Database, + Roles: routeToDatabase.Roles, + } +} + +func resourceIDsToTypes(resourceIDs []*decisionpb.ResourceId) []types.ResourceID { + if len(resourceIDs) == 0 { + return nil + } + + ret := make([]types.ResourceID, len(resourceIDs)) + for i, r := range resourceIDs { + ret[i] = types.ResourceID{ + ClusterName: r.ClusterName, + Kind: r.Kind, + Name: r.Name, + SubResourceName: r.SubResourceName, + } + } + return ret +} + +func resourceIDsFromTypes(resourceIDs []types.ResourceID) []*decisionpb.ResourceId { + if len(resourceIDs) == 0 { + return nil + } + + ret := make([]*decisionpb.ResourceId, len(resourceIDs)) + for i, r := range resourceIDs { + ret[i] = &decisionpb.ResourceId{ + ClusterName: r.ClusterName, + Kind: r.Kind, + Name: r.Name, + SubResourceName: r.SubResourceName, + } + } + return ret +} + +func deviceExtensionsFromProto(exts *decisionpb.DeviceExtensions) tlsca.DeviceExtensions { + if exts == nil { + return tlsca.DeviceExtensions{} + } + + return tlsca.DeviceExtensions{ + DeviceID: exts.DeviceId, + AssetTag: exts.AssetTag, + CredentialID: exts.CredentialId, + } +} + +func deviceExtensionsToProto(exts *tlsca.DeviceExtensions) *decisionpb.DeviceExtensions { + if exts == nil { + return nil + } + + return &decisionpb.DeviceExtensions{ + DeviceId: exts.DeviceID, + AssetTag: exts.AssetTag, + CredentialId: exts.CredentialID, + } +} diff --git a/lib/decision/tls_identity_test.go b/lib/decision/tls_identity_test.go new file mode 100644 index 0000000000000..8ac417c3b47da --- /dev/null +++ b/lib/decision/tls_identity_test.go @@ -0,0 +1,171 @@ +// 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 decision_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" + + decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1" + traitpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trait/v1" + "github.com/gravitational/teleport/lib/decision" + "github.com/gravitational/teleport/lib/tlsca" +) + +func TestTLSIdentity_roundtrip(t *testing.T) { + t.Parallel() + + minimalTLSIdentity := &decisionpb.TLSIdentity{ + // tlsca.Identity has no pointer fields, so these are always non-nil after + // copying. + RouteToApp: &decisionpb.RouteToApp{}, + RouteToDatabase: &decisionpb.RouteToDatabase{}, + DeviceExtensions: &decisionpb.DeviceExtensions{}, + } + + fullIdentity := &decisionpb.TLSIdentity{ + Username: "user", + Impersonator: "impersonator", + Groups: []string{"role1", "role2"}, + SystemRoles: []string{"system1", "system2"}, + Usage: []string{"usage1", "usage2"}, + Principals: []string{"login1", "login2"}, + KubernetesGroups: []string{"kgroup1", "kgroup2"}, + KubernetesUsers: []string{"kuser1", "kuser2"}, + Expires: timestamppb.Now(), + RouteToCluster: "route-to-cluster", + KubernetesCluster: "k8s-cluster", + Traits: []*traitpb.Trait{ + // Note: sorted by key on conversion. + {Key: "", Values: []string{"missingkey"}}, + {Key: "missingvalues", Values: nil}, + {Key: "trait1", Values: []string{"val1"}}, + {Key: "trait2", Values: []string{"val1", "val2"}}, + }, + RouteToApp: &decisionpb.RouteToApp{ + SessionId: "session-id", + PublicAddr: "public-addr", + ClusterName: "cluster-name", + Name: "name", + AwsRoleArn: "aws-role-arn", + AzureIdentity: "azure-id", + GcpServiceAccount: "gcp-service-account", + Uri: "uri", + TargetPort: 111, + }, + TeleportCluster: "teleport-cluster", + RouteToDatabase: &decisionpb.RouteToDatabase{ + ServiceName: "service-name", + Protocol: "protocol", + Username: "username", + Database: "database", + Roles: []string{"role1", "role2"}, + }, + DatabaseNames: []string{"db1", "db2"}, + DatabaseUsers: []string{"dbuser1", "dbuser2"}, + MfaVerified: "mfa-device-id", + PreviousIdentityExpires: timestamppb.Now(), + LoginIp: "login-ip", + PinnedIp: "pinned-ip", + AwsRoleArns: []string{"arn1", "arn2"}, + AzureIdentities: []string{"azure-id-1", "azure-id-2"}, + GcpServiceAccounts: []string{"gcp-account-1", "gcp-account-2"}, + ActiveRequests: []string{"accessrequest1", "accessrequest2"}, + DisallowReissue: true, + Renewable: true, + Generation: 112, + BotName: "bot-name", + BotInstanceId: "bot-instance-id", + AllowedResourceIds: []*decisionpb.ResourceId{ + { + ClusterName: "cluster1", + Kind: "kind1", + Name: "name1", + SubResourceName: "sub-resource1", + }, + { + ClusterName: "cluster2", + Kind: "kind2", + Name: "name2", + SubResourceName: "sub-resource2", + }, + }, + PrivateKeyPolicy: "private-key-policy", + ConnectionDiagnosticId: "connection-diag-id", + DeviceExtensions: &decisionpb.DeviceExtensions{ + DeviceId: "device-id", + AssetTag: "asset-tag", + CredentialId: "credential-id", + }, + UserType: "user-type", + } + + tests := []struct { + name string + start, want *decisionpb.TLSIdentity + }{ + { + name: "nil-to-nil", + start: nil, + want: nil, + }, + { + name: "zero-to-zero", + start: &decisionpb.TLSIdentity{}, + want: minimalTLSIdentity, + }, + { + name: "full identity", + start: fullIdentity, + want: fullIdentity, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := decision.TLSIdentityFromTLSCA( + decision.TLSIdentityToTLSCA(test.start), + ) + if diff := cmp.Diff(test.want, got, protocmp.Transform()); diff != "" { + t.Errorf("TLSIdentity conversion mismatch (-want +got)\n%s", diff) + } + }) + } + + t.Run("zero tlsca.Identity", func(t *testing.T) { + var id tlsca.Identity + got := decision.TLSIdentityFromTLSCA(&id) + want := minimalTLSIdentity + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("TLSIdentity conversion mismatch (-want +got)\n%s", diff) + } + }) +} + +func TestTLSIdentityToTLSCA_zeroTimestamp(t *testing.T) { + t.Parallel() + + id := decision.TLSIdentityToTLSCA(&decisionpb.TLSIdentity{ + Expires: ×tamppb.Timestamp{}, + PreviousIdentityExpires: ×tamppb.Timestamp{}, + }) + assert.Zero(t, id.Expires, "id.Expires") + assert.Zero(t, id.PreviousIdentityExpires, "id.PreviousIdentityExpires") +} From b1d8c3bdf09975937ca60d07e23d591f6c3596e1 Mon Sep 17 00:00:00 2001 From: Zac Bergquist Date: Thu, 19 Dec 2024 11:55:53 -0700 Subject: [PATCH 25/64] devicetrust: don't invoke powershell when reading system information (#50372) The device trust web flow can result in a web browser launching Teleport Connect (which launches tsh, which in turn launches powershell). Some antivirus solutions flag cases where a powershell process is a descendent of a web browser process. In order to avoid being blocked by the antivirus software, we want to read system information directly instead of via powershell. --- go.mod | 2 +- lib/devicetrust/native/device_windows.go | 131 ++++++++++------------- 2 files changed, 59 insertions(+), 74 deletions(-) diff --git a/go.mod b/go.mod index 214e7e3e1714f..eb16212086763 100644 --- a/go.mod +++ b/go.mod @@ -185,6 +185,7 @@ require ( github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb github.com/vulcand/predicate v1.2.0 // replaced github.com/xanzy/go-gitlab v0.114.0 + github.com/yusufpapurcu/wmi v1.2.4 go.etcd.io/etcd/api/v3 v3.5.17 go.etcd.io/etcd/client/v3 v3.5.17 go.mongodb.org/mongo-driver v1.14.0 @@ -521,7 +522,6 @@ require ( github.com/xlab/treeprint v1.2.0 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect github.com/yuin/gopher-lua v1.1.1 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zeebo/errs v1.3.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c // indirect diff --git a/lib/devicetrust/native/device_windows.go b/lib/devicetrust/native/device_windows.go index 036f1e48e514d..b0a871c18a994 100644 --- a/lib/devicetrust/native/device_windows.go +++ b/lib/devicetrust/native/device_windows.go @@ -19,17 +19,18 @@ package native import ( - "bytes" "encoding/base64" "errors" + "fmt" "os" - "os/exec" "os/user" + "strconv" "time" "github.com/google/go-attestation/attest" "github.com/gravitational/trace" log "github.com/sirupsen/logrus" + "github.com/yusufpapurcu/wmi" "golang.org/x/sync/errgroup" "golang.org/x/sys/windows" "google.golang.org/protobuf/types/known/timestamppb" @@ -78,106 +79,90 @@ func handleTPMActivateCredential(encryptedCredential, encryptedCredentialSecret return windowsDevice.handleTPMActivateCredential(encryptedCredential, encryptedCredentialSecret) } -// getDeviceSerial returns the serial number of the device using PowerShell to -// grab the correct WMI objects. Getting it without calling into PS is possible, -// but requires interfacing with the ancient Win32 COM APIs. func getDeviceSerial() (string, error) { - cmd := exec.Command( - "powershell", - "-NoProfile", - "Get-WmiObject Win32_BIOS | Select -ExpandProperty SerialNumber", - ) // ThinkPad P P14s: // PS > Get-WmiObject Win32_BIOS | Select -ExpandProperty SerialNumber // PF47WND6 - out, err := cmd.Output() - if err != nil { + + type Win32_BIOS struct { + SerialNumber string + } + + var bios []Win32_BIOS + query := wmi.CreateQuery(&bios, "") + if err := wmi.Query(query, &bios); err != nil { return "", trace.Wrap(err) } - return string(bytes.TrimSpace(out)), nil + + if len(bios) == 0 { + return "", trace.BadParameter("could not read serial number from Win32_BIOS") + } + + return bios[0].SerialNumber, nil } func getReportedAssetTag() (string, error) { - cmd := exec.Command( - "powershell", - "-NoProfile", - "Get-WmiObject Win32_SystemEnclosure | Select -ExpandProperty SMBIOSAssetTag", - ) // ThinkPad P P14s: // PS > Get-WmiObject Win32_SystemEnclosure | Select -ExpandProperty SMBIOSAssetTag // winaia_1337 - out, err := cmd.Output() - if err != nil { + + type Win32_SystemEnclosure struct { + SMBIOSAssetTag string + } + + var system []Win32_SystemEnclosure + query := wmi.CreateQuery(&system, "") + if err := wmi.Query(query, &system); err != nil { return "", trace.Wrap(err) } - return string(bytes.TrimSpace(out)), nil + + if len(system) == 0 { + return "", trace.BadParameter("could not read asset tag from Win32_SystemEnclosure") + } + + return system[0].SMBIOSAssetTag, nil } func getDeviceModel() (string, error) { - cmd := exec.Command( - "powershell", - "-NoProfile", - "Get-WmiObject Win32_ComputerSystem | Select -ExpandProperty Model", - ) // ThinkPad P P14s: // PS> Get-WmiObject Win32_ComputerSystem | Select -ExpandProperty Model // 21J50013US - out, err := cmd.Output() - if err != nil { + + type Win32_ComputerSystem struct { + Model string + } + var cs []Win32_ComputerSystem + query := wmi.CreateQuery(&cs, "") + if err := wmi.Query(query, &cs); err != nil { return "", trace.Wrap(err) } - return string(bytes.TrimSpace(out)), nil + + if len(cs) == 0 { + return "", trace.BadParameter("could not read model from Win32_ComputerSystem") + } + + return cs[0].Model, nil } func getDeviceBaseBoardSerial() (string, error) { - cmd := exec.Command( - "powershell", - "-NoProfile", - "Get-WmiObject Win32_BaseBoard | Select -ExpandProperty SerialNumber", - ) // ThinkPad P P14s: // PS> Get-WmiObject Win32_BaseBoard | Select -ExpandProperty SerialNumber // L1HF2CM03ZT - out, err := cmd.Output() - if err != nil { - return "", trace.Wrap(err) - } - return string(bytes.TrimSpace(out)), nil -} - -func getOSVersion() (string, error) { - cmd := exec.Command( - "powershell", - "-NoProfile", - "Get-WmiObject Win32_OperatingSystem | Select -ExpandProperty Version", - ) - // ThinkPad P P14s: - // PS> Get-WmiObject Win32_OperatingSystem | Select -ExpandProperty Version - // 10.0.22621 - out, err := cmd.Output() - if err != nil { + type Win32_BaseBoard struct { + SerialNumber string + } + var bb []Win32_BaseBoard + query := wmi.CreateQuery(&bb, "") + if err := wmi.Query(query, &bb); err != nil { return "", trace.Wrap(err) } - return string(bytes.TrimSpace(out)), nil -} - -func getOSBuildNumber() (string, error) { - cmd := exec.Command( - "powershell", - "-NoProfile", - "Get-WmiObject Win32_OperatingSystem | Select -ExpandProperty BuildNumber", - ) - // ThinkPad P P14s: - // PS> Get-WmiObject Win32_OperatingSystem | Select -ExpandProperty BuildNumber - // 22621 - out, err := cmd.Output() - if err != nil { - return "", trace.Wrap(err) + if len(bb) == 0 { + return "", trace.BadParameter("could not read serial from Win32_BaseBoard") } - return string(bytes.TrimSpace(out)), nil + return bb[0].SerialNumber, nil } func collectDeviceData(_ CollectDataMode) (*devicepb.DeviceCollectedData, error) { @@ -188,15 +173,13 @@ func collectDeviceData(_ CollectDataMode) (*devicepb.DeviceCollectedData, error) g.SetLimit(groupLimit) // Run exec-ed commands concurrently. - var systemSerial, baseBoardSerial, reportedAssetTag, model, osVersion, osBuildNumber string + var systemSerial, baseBoardSerial, reportedAssetTag, model string for _, spec := range []struct { fn func() (string, error) out *string desc string }{ {fn: getDeviceModel, out: &model, desc: "device model"}, - {fn: getOSVersion, out: &osVersion, desc: "os version"}, - {fn: getOSBuildNumber, out: &osBuildNumber, desc: "os build number"}, {fn: getDeviceSerial, out: &systemSerial, desc: "system serial"}, {fn: getDeviceBaseBoardSerial, out: &baseBoardSerial, desc: "base board serial"}, {fn: getReportedAssetTag, out: &reportedAssetTag, desc: "reported asset tag"}, @@ -214,6 +197,8 @@ func collectDeviceData(_ CollectDataMode) (*devicepb.DeviceCollectedData, error) }) } + ver := windows.RtlGetVersion() + // We want to fetch as much info as possible, so errors are ignored. _ = g.Wait() @@ -232,8 +217,8 @@ func collectDeviceData(_ CollectDataMode) (*devicepb.DeviceCollectedData, error) OsType: devicepb.OSType_OS_TYPE_WINDOWS, SerialNumber: serial, ModelIdentifier: model, - OsVersion: osVersion, - OsBuild: osBuildNumber, + OsVersion: fmt.Sprintf("%v.%v.%v", ver.MajorVersion, ver.MinorVersion, ver.BuildNumber), + OsBuild: strconv.FormatInt(int64(ver.BuildNumber), 10), OsUsername: u.Username, SystemSerialNumber: systemSerial, BaseBoardSerialNumber: baseBoardSerial, From 3e3daecf112b3a834703ffddcb8f506c45c727d8 Mon Sep 17 00:00:00 2001 From: Hugo Shaka Date: Thu, 19 Dec 2024 13:57:58 -0500 Subject: [PATCH 26/64] add tctl create/get/edit support for autoupdate_agent_rollout (#50393) * add tctl create/get/edit support for autoupdate_agent_rollout * fix bad copy paste --- tool/tctl/common/collection.go | 22 +++++++++ tool/tctl/common/resource_command.go | 45 ++++++++++++++++++ tool/tctl/common/resource_command_test.go | 56 +++++++++++++++++++++++ 3 files changed, 123 insertions(+) diff --git a/tool/tctl/common/collection.go b/tool/tctl/common/collection.go index 3c844a28637a7..c31d2a25ed0bf 100644 --- a/tool/tctl/common/collection.go +++ b/tool/tctl/common/collection.go @@ -1939,6 +1939,28 @@ func (c *autoUpdateVersionCollection) writeText(w io.Writer, verbose bool) error return trace.Wrap(err) } +type autoUpdateAgentRolloutCollection struct { + rollout *autoupdatev1pb.AutoUpdateAgentRollout +} + +func (c *autoUpdateAgentRolloutCollection) resources() []types.Resource { + return []types.Resource{types.Resource153ToLegacy(c.rollout)} +} + +func (c *autoUpdateAgentRolloutCollection) writeText(w io.Writer, verbose bool) error { + t := asciitable.MakeTable([]string{"Name", "Start Version", "Target Version", "Mode", "Schedule", "Strategy"}) + t.AddRow([]string{ + c.rollout.GetMetadata().GetName(), + fmt.Sprintf("%v", c.rollout.GetSpec().GetStartVersion()), + fmt.Sprintf("%v", c.rollout.GetSpec().GetTargetVersion()), + fmt.Sprintf("%v", c.rollout.GetSpec().GetAutoupdateMode()), + fmt.Sprintf("%v", c.rollout.GetSpec().GetSchedule()), + fmt.Sprintf("%v", c.rollout.GetSpec().GetStrategy()), + }) + _, err := t.AsBuffer().WriteTo(w) + return trace.Wrap(err) +} + type accessMonitoringRuleCollection struct { items []*accessmonitoringrulesv1pb.AccessMonitoringRule } diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index e77b7eb4aaf4a..dd9d5ea13af20 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -180,6 +180,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, _ *tctlcfg.Globa types.KindAutoUpdateConfig: rc.createAutoUpdateConfig, types.KindAutoUpdateVersion: rc.createAutoUpdateVersion, types.KindGitServer: rc.createGitServer, + types.KindAutoUpdateAgentRollout: rc.createAutoUpdateAgentRollout, } rc.UpdateHandlers = map[ResourceKind]ResourceCreateHandler{ types.KindUser: rc.updateUser, @@ -201,6 +202,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, _ *tctlcfg.Globa types.KindAutoUpdateVersion: rc.updateAutoUpdateVersion, types.KindDynamicWindowsDesktop: rc.updateDynamicWindowsDesktop, types.KindGitServer: rc.updateGitServer, + types.KindAutoUpdateAgentRollout: rc.updateAutoUpdateAgentRollout, } rc.config = config @@ -1617,6 +1619,7 @@ func (rc *ResourceCommand) Delete(ctx context.Context, client *authclient.Client types.KindNetworkRestrictions, types.KindAutoUpdateConfig, types.KindAutoUpdateVersion, + types.KindAutoUpdateAgentRollout, } if !slices.Contains(singletonResources, rc.ref.Kind) && (rc.ref.Kind == "" || rc.ref.Name == "") { return trace.BadParameter("provide a full resource name to delete, for example:\n$ tctl rm cluster/east\n") @@ -2039,6 +2042,11 @@ func (rc *ResourceCommand) Delete(ctx context.Context, client *authclient.Client return trace.Wrap(err) } fmt.Printf("AutoUpdateVersion has been deleted\n") + case types.KindAutoUpdateAgentRollout: + if err := client.DeleteAutoUpdateAgentRollout(ctx); err != nil { + return trace.Wrap(err) + } + fmt.Printf("AutoUpdateAgentRollout has been deleted\n") default: return trace.BadParameter("deleting resources of type %q is not supported", rc.ref.Kind) } @@ -3283,6 +3291,12 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient return nil, trace.Wrap(err) } return &autoUpdateVersionCollection{version}, nil + case types.KindAutoUpdateAgentRollout: + version, err := client.GetAutoUpdateAgentRollout(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + return &autoUpdateAgentRolloutCollection{version}, nil case types.KindAccessMonitoringRule: if rc.ref.Name != "" { rule, err := client.AccessMonitoringRuleClient().GetAccessMonitoringRule(ctx, rc.ref.Name) @@ -3744,6 +3758,37 @@ func (rc *ResourceCommand) updateAutoUpdateVersion(ctx context.Context, client * return nil } +func (rc *ResourceCommand) createAutoUpdateAgentRollout(ctx context.Context, client *authclient.Client, raw services.UnknownResource) error { + version, err := services.UnmarshalProtoResource[*autoupdatev1pb.AutoUpdateAgentRollout](raw.Raw) + if err != nil { + return trace.Wrap(err) + } + + if rc.IsForced() { + _, err = client.UpsertAutoUpdateAgentRollout(ctx, version) + } else { + _, err = client.CreateAutoUpdateAgentRollout(ctx, version) + } + if err != nil { + return trace.Wrap(err) + } + + fmt.Println("autoupdate_agent_rollout has been created") + return nil +} + +func (rc *ResourceCommand) updateAutoUpdateAgentRollout(ctx context.Context, client *authclient.Client, raw services.UnknownResource) error { + version, err := services.UnmarshalProtoResource[*autoupdatev1pb.AutoUpdateAgentRollout](raw.Raw) + if err != nil { + return trace.Wrap(err) + } + if _, err := client.UpdateAutoUpdateAgentRollout(ctx, version); err != nil { + return trace.Wrap(err) + } + fmt.Println("autoupdate_version has been updated") + return nil +} + func (rc *ResourceCommand) createGitServer(ctx context.Context, client *authclient.Client, raw services.UnknownResource) error { server, err := services.UnmarshalGitServer(raw.Raw) if err != nil { diff --git a/tool/tctl/common/resource_command_test.go b/tool/tctl/common/resource_command_test.go index b3e13e9bffb6d..61b2c2650f53a 100644 --- a/tool/tctl/common/resource_command_test.go +++ b/tool/tctl/common/resource_command_test.go @@ -1427,6 +1427,10 @@ func TestCreateResources(t *testing.T) { kind: types.KindAutoUpdateVersion, create: testCreateAutoUpdateVersion, }, + { + kind: types.KindAutoUpdateAgentRollout, + create: testCreateAutoUpdateAgentRollout, + }, { kind: types.KindDynamicWindowsDesktop, create: testCreateDynamicWindowsDesktop, @@ -2387,6 +2391,58 @@ version: v1 require.ErrorContains(t, err, "autoupdate_version \"autoupdate-version\" doesn't exist") } +func testCreateAutoUpdateAgentRollout(t *testing.T, clt *authclient.Client) { + const resourceYAML = `kind: autoupdate_agent_rollout +metadata: + name: autoupdate-agent-rollout + revision: 3a43b44a-201e-4d7f-aef1-ae2f6d9811ed +spec: + start_version: 1.2.3 + target_version: 1.2.3 + autoupdate_mode: "suspended" + schedule: "regular" + strategy: "halt-on-error" +status: + groups: + - name: my-group + state: 1 + config_days: ["*"] + config_start_hour: 12 + config_wait_hours: 0 +version: v1 +` + _, err := runResourceCommand(t, clt, []string{"get", types.KindAutoUpdateAgentRollout, "--format=json"}) + require.ErrorContains(t, err, "doesn't exist") + + // Create the resource. + resourceYAMLPath := filepath.Join(t.TempDir(), "resource.yaml") + require.NoError(t, os.WriteFile(resourceYAMLPath, []byte(resourceYAML), 0644)) + _, err = runResourceCommand(t, clt, []string{"create", resourceYAMLPath}) + require.NoError(t, err) + + // Get the resource + buf, err := runResourceCommand(t, clt, []string{"get", types.KindAutoUpdateAgentRollout, "--format=json"}) + require.NoError(t, err) + resources := mustDecodeJSON[[]*autoupdate.AutoUpdateAgentRollout](t, buf) + require.Len(t, resources, 1) + + var expected autoupdate.AutoUpdateAgentRollout + require.NoError(t, yaml.Unmarshal([]byte(resourceYAML), &expected)) + + require.Empty(t, cmp.Diff( + []*autoupdate.AutoUpdateAgentRollout{&expected}, + resources, + protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"), + protocmp.Transform(), + )) + + // Delete the resource + _, err = runResourceCommand(t, clt, []string{"rm", types.KindAutoUpdateAgentRollout}) + require.NoError(t, err) + _, err = runResourceCommand(t, clt, []string{"get", types.KindAutoUpdateAgentRollout}) + require.ErrorContains(t, err, "autoupdate_agent_rollout \"autoupdate-agent-rollout\" doesn't exist") +} + func testCreateDynamicWindowsDesktop(t *testing.T, clt *authclient.Client) { const resourceYAML = `kind: dynamic_windows_desktop metadata: From 6b80477a14a0d7ee69ea78bee64b0b381a322d42 Mon Sep 17 00:00:00 2001 From: rosstimothy <39066650+rosstimothy@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:35:18 -0500 Subject: [PATCH 27/64] Convert lib/web to use slog (#50407) --- integration/kube_integration_test.go | 4 +- lib/service/service.go | 6 +- lib/web/addr.go | 9 +- lib/web/apiserver.go | 230 ++++++++++++++------------- lib/web/apiserver_test.go | 10 +- lib/web/apiserver_test_utils.go | 4 +- lib/web/apps.go | 14 +- lib/web/autoupdate_rfd109.go | 4 +- lib/web/desktop.go | 67 +++++--- lib/web/desktop/playback.go | 21 +-- lib/web/desktop/playback_test.go | 3 +- lib/web/desktop_playback.go | 6 +- lib/web/device_trust.go | 18 +-- lib/web/features.go | 14 +- lib/web/features_test.go | 1 - lib/web/headless.go | 2 +- lib/web/integrations_awsoidc.go | 2 +- lib/web/join_tokens.go | 37 +++-- lib/web/kube.go | 64 +++++--- lib/web/server.go | 13 +- lib/web/sessions.go | 146 +++++++++-------- lib/web/terminal.go | 89 +++++------ lib/web/terminal/terminal.go | 39 ++--- lib/web/terminal_test.go | 5 +- lib/web/tty_playback.go | 33 ++-- lib/web/ui/app.go | 8 +- lib/web/user_groups.go | 4 +- lib/web/web.go | 34 ---- 28 files changed, 459 insertions(+), 428 deletions(-) delete mode 100644 lib/web/web.go diff --git a/integration/kube_integration_test.go b/integration/kube_integration_test.go index ad7d745d89452..c9fd5ff21efdb 100644 --- a/integration/kube_integration_test.go +++ b/integration/kube_integration_test.go @@ -1768,7 +1768,7 @@ func testKubeExecWeb(t *testing.T, suite *KubeSuite) { ws := openWebsocketAndReadSession(t, endpoint, req) - wsStream := terminal.NewWStream(context.Background(), ws, suite.log, nil) + wsStream := terminal.NewWStream(context.Background(), ws, utils.NewSlogLoggerForTests(), nil) // Check for the expected string in the output. findTextInReader(t, wsStream, testNamespace, time.Second*2) @@ -1789,7 +1789,7 @@ func testKubeExecWeb(t *testing.T, suite *KubeSuite) { ws := openWebsocketAndReadSession(t, endpoint, req) - wsStream := terminal.NewWStream(context.Background(), ws, suite.log, nil) + wsStream := terminal.NewWStream(context.Background(), ws, utils.NewSlogLoggerForTests(), nil) // Read first prompt from the server. readData := make([]byte, 255) diff --git a/lib/service/service.go b/lib/service/service.go index 39666c2aa1d91..ebdb99b6a282b 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -4723,7 +4723,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { }, }, Handler: webHandler, - Log: process.log.WithField(teleport.ComponentKey, teleport.Component(teleport.ComponentReverseTunnelServer, process.id)), + Log: process.logger.With(teleport.ComponentKey, teleport.Component(teleport.ComponentReverseTunnelServer, process.id)), }) if err != nil { return trace.Wrap(err) @@ -5517,7 +5517,7 @@ func (process *TeleportProcess) initMinimalReverseTunnel(listeners *proxyListene return nil }) - log := process.log.WithField(teleport.ComponentKey, teleport.Component(teleport.ComponentReverseTunnelServer, process.id)) + log := process.logger.With(teleport.ComponentKey, teleport.Component(teleport.ComponentReverseTunnelServer, process.id)) minimalWebServer, err := web.NewServer(web.ServerConfig{ Server: &http.Server{ @@ -5526,7 +5526,7 @@ func (process *TeleportProcess) initMinimalReverseTunnel(listeners *proxyListene ReadHeaderTimeout: defaults.ReadHeadersTimeout, WriteTimeout: apidefaults.DefaultIOTimeout, IdleTimeout: apidefaults.DefaultIdleTimeout, - ErrorLog: utils.NewStdlogger(log.Error, teleport.ComponentReverseTunnelServer), + ErrorLog: slog.NewLogLogger(log.Handler(), slog.LevelError), }, Handler: minimalWebHandler, Log: log, diff --git a/lib/web/addr.go b/lib/web/addr.go index f82173b6f7796..3c23bf0637c85 100644 --- a/lib/web/addr.go +++ b/lib/web/addr.go @@ -20,13 +20,14 @@ package web import ( "bufio" + "context" + "log/slog" "net" "net/http" "net/netip" "strings" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport/lib/authz" "github.com/gravitational/teleport/lib/utils" @@ -56,7 +57,7 @@ func NewXForwardedForMiddleware(next http.Handler) http.Handler { // Serve with updated client source address. default: next.ServeHTTP( - responseWriterWithClientSrcAddr(w, clientSrcAddr), + responseWriterWithClientSrcAddr(r.Context(), w, clientSrcAddr), requestWithClientSrcAddr(r, clientSrcAddr), ) } @@ -113,11 +114,11 @@ func requestWithClientSrcAddr(r *http.Request, clientSrcAddr net.Addr) *http.Req return r } -func responseWriterWithClientSrcAddr(w http.ResponseWriter, clientSrcAddr net.Addr) http.ResponseWriter { +func responseWriterWithClientSrcAddr(ctx context.Context, w http.ResponseWriter, clientSrcAddr net.Addr) http.ResponseWriter { // Returns the original ResponseWriter if not a http.Hijacker. _, ok := w.(http.Hijacker) if !ok { - logrus.Debug("Provided ResponseWriter is not a hijacker.") + slog.DebugContext(ctx, "Provided ResponseWriter is not a hijacker") return w } diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 597360e64df8a..a81932f586de4 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -49,7 +49,6 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/julienschmidt/httprouter" - "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" oteltrace "go.opentelemetry.io/otel/trace" tracepb "go.opentelemetry.io/proto/otlp/trace/v1" @@ -102,6 +101,7 @@ import ( "github.com/gravitational/teleport/lib/session" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" + logutils "github.com/gravitational/teleport/lib/utils/log" "github.com/gravitational/teleport/lib/web/app" websession "github.com/gravitational/teleport/lib/web/session" "github.com/gravitational/teleport/lib/web/terminal" @@ -146,8 +146,6 @@ type healthCheckAppServerFunc func(ctx context.Context, publicAddr string, clust // Handler is HTTP web proxy handler type Handler struct { - // TODO(greedy52) deprecate logrus.FieldLogger. - log logrus.FieldLogger logger *slog.Logger sync.Mutex @@ -356,12 +354,12 @@ func (h *APIHandler) handlePreflight(w http.ResponseWriter, r *http.Request) { servers, err := app.Match(r.Context(), h.handler.cfg.AccessPoint, app.MatchPublicAddr(publicAddr)) if err != nil { - h.handler.log.Info("failed to match application with public addr %s", publicAddr) + h.handler.logger.InfoContext(r.Context(), "failed to match application with public addr", "public_addr", publicAddr) return } if len(servers) == 0 { - h.handler.log.Info("failed to match application with public addr %s", publicAddr) + h.handler.logger.InfoContext(r.Context(), "failed to match application with public addr", "public_addr", publicAddr) return } @@ -454,7 +452,6 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { h := &Handler{ cfg: cfg, - log: newPackageLogger(), logger: slog.Default().With(teleport.ComponentKey, teleport.ComponentWeb), clock: clockwork.NewRealClock(), clusterFeatures: cfg.ClusterFeatures, @@ -512,6 +509,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { clock: h.clock, sessionLingeringThreshold: sessionLingeringThreshold, proxySigner: cfg.PROXYSigner, + logger: h.logger, }) if err != nil { return nil, trace.Wrap(err) @@ -521,8 +519,11 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { if cfg.ProxySSHAddr.String() != "" { _, sshPort, err := net.SplitHostPort(cfg.ProxySSHAddr.String()) if err != nil { - h.log.WithError(err).Warnf("Invalid SSH proxy address %q, will use default port %v.", - cfg.ProxySSHAddr.String(), defaults.SSHProxyListenPort) + h.logger.WarnContext(h.cfg.Context, "Invalid SSH proxy address, will use default port", + "error", err, + "ssh_proxy_addr", logutils.StringerAttr(&cfg.ProxySSHAddr), + "default_port", defaults.SSHProxyListenPort, + ) } else { sshPortValue = sshPort } @@ -581,7 +582,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { if cfg.StaticFS != nil { index, err := cfg.StaticFS.Open("/index.html") if err != nil { - h.log.WithError(err).Error("Failed to open index file.") + h.logger.ErrorContext(h.cfg.Context, "Failed to open index file", "error", err) return nil, trace.Wrap(err) } defer index.Close() @@ -598,7 +599,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { etagFromAppHash, err := readEtagFromAppHash(cfg.StaticFS) if err != nil { - h.log.WithError(err).Error("Could not read apphash from embedded webassets. Using version only as ETag for Web UI assets.") + h.logger.ErrorContext(h.cfg.Context, "Could not read apphash from embedded webassets. Using version only as ETag for Web UI assets", "error", err) } else { etag = etagFromAppHash } @@ -653,12 +654,12 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { } else if strings.HasPrefix(r.URL.Path, "/web/") || r.URL.Path == "/web" { csrfToken, err := csrf.AddCSRFProtection(w, r) if err != nil { - h.log.WithError(err).Warn("Failed to generate CSRF token.") + h.logger.WarnContext(r.Context(), "Failed to generate CSRF token", "error", err) } session, err := h.authenticateWebSession(w, r) if err != nil { - h.log.Debugf("Could not authenticate: %v", err) + h.logger.DebugContext(r.Context(), "Could not authenticate", "error", err) } session.XCSRF = csrfToken @@ -666,7 +667,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { httplib.SetIndexContentSecurityPolicy(w.Header(), cfg.ClusterFeatures, r.URL.Path) if err := indexPage.Execute(w, session); err != nil { - h.log.WithError(err).Error("Failed to execute index page template.") + h.logger.ErrorContext(r.Context(), "Failed to execute index page template", "error", err) } } else { http.NotFound(w, r) @@ -1274,7 +1275,7 @@ func (h *Handler) AccessGraphAddr() utils.NetAddr { return h.cfg.AccessGraphAddr } -func localSettings(cap types.AuthPreference) (webclient.AuthenticationSettings, error) { +func localSettings(ctx context.Context, cap types.AuthPreference, logger *slog.Logger) (webclient.AuthenticationSettings, error) { as := webclient.AuthenticationSettings{ Type: constants.Local, SecondFactor: types.LegacySecondFactorFromSecondFactors(cap.GetSecondFactors()), @@ -1298,7 +1299,7 @@ func localSettings(cap types.AuthPreference) (webclient.AuthenticationSettings, case err == nil: as.U2F = &webclient.U2FSettings{AppID: u2f.AppID} case !trace.IsNotFound(err): - log.WithError(err).Warnf("Error reading U2F settings") + logger.WarnContext(ctx, "Error reading U2F settings", "error", err) } // Webauthn settings. @@ -1308,7 +1309,7 @@ func localSettings(cap types.AuthPreference) (webclient.AuthenticationSettings, RPID: webConfig.RPID, } case !trace.IsNotFound(err): - log.WithError(err).Warnf("Error reading WebAuthn settings") + logger.WarnContext(ctx, "Error reading WebAuthn settings", "error", err) } return as, nil @@ -1386,7 +1387,7 @@ func deviceTrustDisabled(cap types.AuthPreference) bool { return dtconfig.GetEffectiveMode(cap.GetDeviceTrust()) == constants.DeviceTrustModeOff } -func getAuthSettings(ctx context.Context, authClient authclient.ClientI) (webclient.AuthenticationSettings, error) { +func getAuthSettings(ctx context.Context, authClient authclient.ClientI, logger *slog.Logger) (webclient.AuthenticationSettings, error) { authPreference, err := authClient.GetAuthPreference(ctx) if err != nil { return webclient.AuthenticationSettings{}, trace.Wrap(err) @@ -1396,7 +1397,7 @@ func getAuthSettings(ctx context.Context, authClient authclient.ClientI) (webcli switch authPreference.GetType() { case constants.Local: - as, err = localSettings(authPreference) + as, err = localSettings(ctx, authPreference, logger) if err != nil { return webclient.AuthenticationSettings{}, trace.Wrap(err) } @@ -1475,18 +1476,18 @@ func getAuthSettings(ctx context.Context, authClient authclient.ClientI) (webcli func (h *Handler) traces(w http.ResponseWriter, r *http.Request, _ httprouter.Params, _ *SessionContext) (interface{}, error) { body, err := utils.ReadAtMost(r.Body, teleport.MaxHTTPResponseSize) if err != nil { - h.log.WithError(err).Error("Failed to read traces request") + h.logger.ErrorContext(r.Context(), "Failed to read traces request", "error", err) w.WriteHeader(http.StatusBadRequest) return nil, nil } if err := r.Body.Close(); err != nil { - h.log.WithError(err).Warn("Failed to close traces request body") + h.logger.WarnContext(r.Context(), "Failed to close traces request body", "error", err) } var data tracepb.TracesData if err := protojson.Unmarshal(body, &data); err != nil { - h.log.WithError(err).Error("Failed to unmarshal traces request") + h.logger.ErrorContext(r.Context(), "Failed to unmarshal traces request", "error", err) w.WriteHeader(http.StatusBadRequest) return nil, nil } @@ -1531,7 +1532,7 @@ func (h *Handler) traces(w http.ResponseWriter, r *http.Request, _ httprouter.Pa ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := h.cfg.TraceClient.UploadTraces(ctx, data.ResourceSpans); err != nil { - h.log.WithError(err).Error("Failed to upload traces") + h.logger.ErrorContext(ctx, "Failed to upload traces", "error", err) } }() @@ -1541,7 +1542,7 @@ func (h *Handler) traces(w http.ResponseWriter, r *http.Request, _ httprouter.Pa func (h *Handler) ping(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { var err error - authSettings, err := getAuthSettings(r.Context(), h.cfg.ProxyClient) + authSettings, err := getAuthSettings(r.Context(), h.cfg.ProxyClient, h.logger) if err != nil { return nil, trace.Wrap(err) } @@ -1635,7 +1636,7 @@ func (h *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p ht hasMessageOfTheDay := cap.GetMessageOfTheDay() != "" if slices.Contains(constants.SystemConnectors, connectorName) { - response.Auth, err = localSettings(cap) + response.Auth, err = localSettings(r.Context(), cap, h.logger) if err != nil { return nil, trace.Wrap(err) } @@ -1706,7 +1707,7 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou // get all OIDC connectors oidcConnectors, err := h.cfg.ProxyClient.GetOIDCConnectors(r.Context(), false) if err != nil { - h.log.WithError(err).Error("Cannot retrieve OIDC connectors.") + h.logger.ErrorContext(r.Context(), "Cannot retrieve OIDC connectors", "error", err) } for _, item := range oidcConnectors { authProviders = append(authProviders, webclient.WebConfigAuthProvider{ @@ -1720,7 +1721,7 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou // get all SAML connectors samlConnectors, err := h.cfg.ProxyClient.GetSAMLConnectors(r.Context(), false) if err != nil { - h.log.WithError(err).Error("Cannot retrieve SAML connectors.") + h.logger.ErrorContext(r.Context(), "Cannot retrieve SAML connectors", "error", err) } for _, item := range samlConnectors { authProviders = append(authProviders, webclient.WebConfigAuthProvider{ @@ -1734,7 +1735,7 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou // get all Github connectors githubConnectors, err := h.cfg.ProxyClient.GetGithubConnectors(r.Context(), false) if err != nil { - h.log.WithError(err).Error("Cannot retrieve GitHub connectors.") + h.logger.ErrorContext(r.Context(), "Cannot retrieve GitHub connectors", "error", err) } for _, item := range githubConnectors { authProviders = append(authProviders, webclient.WebConfigAuthProvider{ @@ -1748,7 +1749,7 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou // get auth type & second factor type var authSettings webclient.WebConfigAuthSettings if cap, err := h.cfg.ProxyClient.GetAuthPreference(r.Context()); err != nil { - h.log.WithError(err).Error("Cannot retrieve AuthPreferences.") + h.logger.ErrorContext(r.Context(), "Cannot retrieve AuthPreferences", "error", err) authSettings = webclient.WebConfigAuthSettings{ Providers: authProviders, SecondFactor: constants.SecondFactorOff, @@ -1782,7 +1783,7 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou tunnelPublicAddr := "" proxyConfig, err := h.cfg.ProxySettings.GetProxySettings(r.Context()) if err != nil { - h.log.WithError(err).Warn("Cannot retrieve ProxySettings, tunnel address won't be set in Web UI.") + h.logger.WarnContext(r.Context(), "Cannot retrieve ProxySettings, tunnel address won't be set in Web UI", "error", err) } else { if clusterFeatures.GetCloud() { tunnelPublicAddr = proxyConfig.SSH.TunnelPublicAddr @@ -1793,7 +1794,7 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou canJoinSessions := true recCfg, err := h.cfg.ProxyClient.GetSessionRecordingConfig(r.Context()) if err != nil { - h.log.WithError(err).Error("Cannot retrieve SessionRecordingConfig.") + h.logger.ErrorContext(r.Context(), "Cannot retrieve SessionRecordingConfig", "error", err) } else { canJoinSessions = !services.IsRecordAtProxy(recCfg.GetMode()) } @@ -1803,7 +1804,7 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou if automaticUpgradesEnabled { automaticUpgradesTargetVersion, err = h.cfg.AutomaticUpgradesChannels.DefaultVersion(r.Context()) if err != nil { - h.log.WithError(err).Error("Cannot read target version") + h.logger.ErrorContext(r.Context(), "Cannot read target version", "error", err) } } @@ -1834,7 +1835,7 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou resource, err := h.cfg.ProxyClient.GetClusterName() if err != nil { - h.log.WithError(err).Warn("Failed to query cluster name.") + h.logger.WarnContext(r.Context(), "Failed to query cluster name", "error", err) } else { webCfg.ProxyClusterName = resource.GetClusterName() } @@ -1979,18 +1980,18 @@ func (h *Handler) motd(w http.ResponseWriter, r *http.Request, p httprouter.Para } func (h *Handler) githubLoginWeb(w http.ResponseWriter, r *http.Request, p httprouter.Params) string { - logger := h.log.WithField("auth", "github") - logger.Debug("Web login start.") + logger := h.logger.With("auth", "github") + logger.DebugContext(r.Context(), "Web login start") req, err := ParseSSORequestParams(r) if err != nil { - logger.WithError(err).Error("Failed to extract SSO parameters from request.") + logger.ErrorContext(r.Context(), "Failed to extract SSO parameters from request", "error", err) return client.LoginFailedRedirectURL } remoteAddr, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - logger.WithError(err).Error("Failed to parse request remote address.") + logger.ErrorContext(r.Context(), "Failed to parse request remote address", "error", err) return client.LoginFailedRedirectURL } @@ -2002,7 +2003,7 @@ func (h *Handler) githubLoginWeb(w http.ResponseWriter, r *http.Request, p httpr ClientUserAgent: r.UserAgent(), }) if err != nil { - logger.WithError(err).Error("Error creating auth request.") + logger.ErrorContext(r.Context(), "Error creating auth request", "error", err) return client.LoginFailedRedirectURL } @@ -2010,23 +2011,23 @@ func (h *Handler) githubLoginWeb(w http.ResponseWriter, r *http.Request, p httpr } func (h *Handler) githubLoginConsole(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { - logger := h.log.WithField("auth", "github") - logger.Debug("Console login start.") + logger := h.logger.With("auth", "github") + logger.DebugContext(r.Context(), "Console login start") req := new(client.SSOLoginConsoleReq) if err := httplib.ReadResourceJSON(r, req); err != nil { - logger.WithError(err).Error("Error reading json.") + logger.ErrorContext(r.Context(), "Error reading json", "error", err) return nil, trace.AccessDenied(SSOLoginFailureMessage) } if err := req.CheckAndSetDefaults(); err != nil { - logger.WithError(err).Error("Missing request parameters.") + logger.ErrorContext(r.Context(), "Missing request parameters", "error", err) return nil, trace.AccessDenied(SSOLoginFailureMessage) } remoteAddr, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - logger.WithError(err).Error("Failed to parse request remote address.") + logger.ErrorContext(r.Context(), "Failed to parse request remote address", "error", err) return nil, trace.AccessDenied(SSOLoginFailureMessage) } @@ -2044,7 +2045,7 @@ func (h *Handler) githubLoginConsole(w http.ResponseWriter, r *http.Request, p h ClientLoginIP: remoteAddr, }) if err != nil { - logger.WithError(err).Error("Failed to create GitHub auth request.") + logger.ErrorContext(r.Context(), "Failed to create GitHub auth request", "error", err) if strings.Contains(err.Error(), auth.InvalidClientRedirectErrorMessage) { return nil, trace.AccessDenied(SSOLoginFailureInvalidRedirect) } @@ -2057,12 +2058,12 @@ func (h *Handler) githubLoginConsole(w http.ResponseWriter, r *http.Request, p h } func (h *Handler) githubCallback(w http.ResponseWriter, r *http.Request, p httprouter.Params) string { - logger := h.log.WithField("auth", "github") - logger.Debugf("Callback start: %v.", r.URL.Query()) + logger := h.logger.With("auth", "github") + logger.DebugContext(r.Context(), "Callback start", "query", r.URL.Query()) response, err := h.cfg.ProxyClient.ValidateGithubAuthCallback(r.Context(), r.URL.Query()) if err != nil { - logger.WithError(err).Error("Error while processing callback.") + logger.ErrorContext(r.Context(), "Error while processing callback", "error", err) // try to find the auth request, which bears the original client redirect URL. // if found, use it to terminate the flow. @@ -2084,7 +2085,7 @@ func (h *Handler) githubCallback(w http.ResponseWriter, r *http.Request, p httpr // if we created web session, set session cookie and redirect to original url if response.Req.CreateWebSession { - logger.Infof("Redirecting to web browser.") + logger.InfoContext(r.Context(), "Redirecting to web browser") res := &SSOCallbackResponse{ CSRFToken: response.Req.CSRFToken, @@ -2094,26 +2095,26 @@ func (h *Handler) githubCallback(w http.ResponseWriter, r *http.Request, p httpr } if err := SSOSetWebSessionAndRedirectURL(w, r, res, true); err != nil { - logger.WithError(err).Error("Error setting web session.") + logger.ErrorContext(r.Context(), "Error setting web session.", "error", err) return client.LoginFailedRedirectURL } if dwt := response.Session.GetDeviceWebToken(); dwt != nil { - logger.Debug("GitHub WebSession created with device web token") + logger.DebugContext(r.Context(), "GitHub WebSession created with device web token") // if a device web token is present, we must send the user to the device authorize page // to upgrade the session. redirectPath, err := BuildDeviceWebRedirectPath(dwt, res.ClientRedirectURL) if err != nil { - logger.WithError(err).Debug("Invalid device web token.") + logger.DebugContext(r.Context(), "Invalid device web token", "error", err) } return redirectPath } return res.ClientRedirectURL } - logger.Infof("Callback is redirecting to console login.") + logger.InfoContext(r.Context(), "Callback is redirecting to console login") if len(response.Req.SSHPubKey)+len(response.Req.TLSPubKey) == 0 { - logger.Error("Not a web or console login request.") + logger.ErrorContext(r.Context(), "Not a web or console login request") return client.LoginFailedRedirectURL } @@ -2128,7 +2129,7 @@ func (h *Handler) githubCallback(w http.ResponseWriter, r *http.Request, p httpr FIPS: h.cfg.FIPS, }) if err != nil { - logger.WithError(err).Error("Error constructing ssh response") + logger.ErrorContext(r.Context(), "Error constructing ssh response", "error", err) return client.LoginFailedRedirectURL } @@ -2407,7 +2408,7 @@ func (h *Handler) createWebSession(w http.ResponseWriter, r *http.Request, p htt return nil, trace.AccessDenied("direct login with password+otp not supported by this cluster") } if err != nil { - h.log.WithError(err).Warnf("Access attempt denied for user %q.", req.User) + h.logger.WarnContext(r.Context(), "Access attempt denied for user", "user", req.User, "error", err) // Since checking for private key policy meant that they passed authn, // return policy error as is to help direct user. if keys.IsPrivateKeyPolicyError(err) { @@ -2423,7 +2424,7 @@ func (h *Handler) createWebSession(w http.ResponseWriter, r *http.Request, p htt ctx, err := h.auth.newSessionContextFromSession(r.Context(), webSession) if err != nil { - h.log.WithError(err).Warnf("Access attempt denied for user %q.", req.User) + h.logger.WarnContext(r.Context(), "Access attempt denied for user", "user", req.User, "error", err) return nil, trace.AccessDenied("need auth") } @@ -2453,9 +2454,10 @@ func clientMetaFromReq(r *http.Request) *authclient.ForwardedClientMetadata { func (h *Handler) deleteWebSession(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) { clt, err := ctx.GetClient() if err != nil { - h.log. - WithError(err). - Warnf("Failed to retrieve user client, SAML single logout will be skipped for user %s.", ctx.GetUser()) + h.logger.WarnContext(r.Context(), "Failed to retrieve user client, SAML single logout will be skipped for user", + "user", ctx.GetUser(), + "error", err, + ) } var user types.User @@ -2463,9 +2465,10 @@ func (h *Handler) deleteWebSession(w http.ResponseWriter, r *http.Request, _ htt if err == nil { user, err = clt.GetUser(r.Context(), ctx.GetUser(), false) if err != nil { - h.log. - WithError(err). - Warnf("Failed to retrieve user during logout, SAML single logout will be skipped for user %s.", ctx.GetUser()) + h.logger.WarnContext(r.Context(), "Failed to retrieve user during logout, SAML single logout will be skipped for user", + "user", ctx.GetUser(), + "error", err, + ) } } @@ -2486,17 +2489,17 @@ func (h *Handler) deleteWebSession(w http.ResponseWriter, r *http.Request, _ htt func (h *Handler) logout(ctx context.Context, w http.ResponseWriter, sctx *SessionContext) error { if err := sctx.Invalidate(ctx); err != nil { - h.log. - WithError(err). - WithField("user", sctx.GetUser()). - Warn("Failed to invalidate sessions") + h.logger.WarnContext(ctx, "Failed to invalidate sessions", + "user", sctx.GetUser(), + "error", err, + ) } - if err := h.auth.releaseResources(sctx.GetUser(), sctx.GetSessionID()); err != nil { - h.log. - WithError(err). - WithField("session_id", sctx.GetSessionID()). - Debug("sessionCache: Failed to release web session") + if err := h.auth.releaseResources(ctx, sctx.GetUser(), sctx.GetSessionID()); err != nil { + h.logger.DebugContext(ctx, "sessionCache: Failed to release web session", + "session_id", sctx.GetSessionID(), + "error", err, + ) } clearSessionCookies(w) @@ -2679,7 +2682,7 @@ func (h *Handler) createResetPasswordToken(w http.ResponseWriter, r *http.Reques func (h *Handler) getResetPasswordTokenHandle(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { result, err := h.getResetPasswordToken(r.Context(), p.ByName("token")) if err != nil { - h.log.WithError(err).Warn("Failed to fetch a reset password token.") + h.logger.WarnContext(r.Context(), "Failed to fetch a reset password token", "error", err) // We hide the error from the remote user to avoid giving any hints. return nil, trace.AccessDenied("bad or expired token") } @@ -3039,7 +3042,7 @@ func (h *Handler) getUserGroupLookup(ctx context.Context, clt apiclient.GetResou UseSearchAsRoles: true, }) if err != nil { - h.log.Infof("Unable to fetch user groups while listing applications, unable to display associated user groups: %v", err) + h.logger.InfoContext(ctx, "Unable to fetch user groups while listing applications, unable to display associated user groups", "error", err) } for _, userGroup := range userGroups { @@ -3109,7 +3112,7 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt AppClusterName: site.GetName(), AllowedAWSRolesLookup: allowedAWSRolesLookup, UserGroupLookup: getUserGroupLookup(), - Logger: h.log, + Logger: h.logger, RequiresRequest: enriched.RequiresRequest, }) unifiedResources = append(unifiedResources, app) @@ -3129,7 +3132,7 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt AppClusterName: site.GetName(), AllowedAWSRolesLookup: allowedAWSRolesLookup, UserGroupLookup: getUserGroupLookup(), - Logger: h.log, + Logger: h.logger, RequiresRequest: enriched.RequiresRequest, }) unifiedResources = append(unifiedResources, app) @@ -3533,9 +3536,9 @@ func (h *Handler) siteNodeConnect( clusterName := site.GetName() if req.SessionID.IsZero() { // An existing session ID was not provided so we need to create a new one. - sessionData, err = h.generateSession(&req, clusterName, sessionCtx) + sessionData, err = h.generateSession(r.Context(), &req, clusterName, sessionCtx) if err != nil { - h.log.WithError(err).Debug("Unable to generate new ssh session.") + h.logger.DebugContext(r.Context(), "Unable to generate new ssh session", "error", err) return nil, trace.Wrap(err) } } else { @@ -3556,19 +3559,23 @@ func (h *Handler) siteNodeConnect( } } - h.log.Debugf("New terminal request for server=%s, login=%s, sid=%s, websid=%s.", - req.Server, req.Login, sessionData.ID, sessionCtx.GetSessionID()) + h.logger.DebugContext(r.Context(), "New terminal request", + "server", req.Server, + "login", req.Login, + "sid", sessionData.ID, + "websid", sessionCtx.GetSessionID(), + ) authAccessPoint, err := site.CachingAccessPoint() if err != nil { - h.log.Debugf("Unable to get auth access point: %v", err) + h.logger.DebugContext(r.Context(), "Unable to get auth access point", "error", err) return nil, trace.Wrap(err) } dialTimeout := apidefaults.DefaultIOTimeout keepAliveInterval := apidefaults.KeepAliveInterval() if netConfig, err := authAccessPoint.GetClusterNetworkingConfig(ctx); err != nil { - h.log.WithError(err).Debug("Unable to fetch cluster networking config.") + h.logger.DebugContext(r.Context(), "Unable to fetch cluster networking config", "error", err) } else { dialTimeout = netConfig.GetSSHDialTimeout() keepAliveInterval = netConfig.GetKeepAliveInterval() @@ -3621,7 +3628,7 @@ func (h *Handler) siteNodeConnect( }, }) if err != nil { - h.log.WithError(err).Error("Unable to create terminal.") + h.logger.ErrorContext(r.Context(), "Unable to create terminal", "error", err) return nil, trace.Wrap(err) } @@ -3629,7 +3636,6 @@ func (h *Handler) siteNodeConnect( defer h.userConns.Add(-1) // start the websocket session with a web-based terminal: - h.log.Infof("Getting terminal to %#v.", req) httplib.MakeTracingHandler(term, teleport.ComponentProxy).ServeHTTP(w, r) return nil, nil @@ -3703,8 +3709,13 @@ func (h *Handler) podConnect( Command: execReq.Command, } - h.log.Debugf("New kube exec request for namespace=%s pod=%s container=%s, sid=%s, websid=%s.", - execReq.Namespace, execReq.Pod, execReq.Container, sess.ID, sctx.GetSessionID()) + h.logger.DebugContext(r.Context(), "New kube exec request", + "namespace", execReq.Namespace, + "pod", execReq.Pod, + "container", execReq.Container, + "sid", sess.ID, + "websid", sctx.GetSessionID(), + ) authAccessPoint, err := site.CachingAccessPoint() if err != nil { @@ -3738,7 +3749,6 @@ func (h *Handler) podConnect( teleportCluster: site.GetName(), ws: ws, keepAliveInterval: keepAliveInterval, - log: h.log.WithField(teleport.ComponentKey, "pod"), logger: h.logger.With(teleport.ComponentKey, "pod"), userClient: clt, localCA: hostCA, @@ -3802,9 +3812,9 @@ func (h *Handler) getKubeExecClusterData(netConfig types.ClusterNetworkingConfig return "https://" + net.JoinHostPort(host, port), tlsServerName, nil } -func (h *Handler) generateSession(req *TerminalRequest, clusterName string, scx *SessionContext) (session.Session, error) { +func (h *Handler) generateSession(ctx context.Context, req *TerminalRequest, clusterName string, scx *SessionContext) (session.Session, error) { owner := scx.cfg.User - h.log.Infof("Generating new session for %s\n", clusterName) + h.logger.InfoContext(ctx, "Generating new session", "cluster", clusterName) host, port, err := serverHostPort(req.Server) if err != nil { @@ -3840,7 +3850,7 @@ func (h *Handler) fetchExistingSession(ctx context.Context, clt authclient.Clien if err != nil { return session.Session{}, nil, trace.Wrap(err) } - h.log.Infof("Attempting to join existing session: %s\n", sessionID) + h.logger.InfoContext(ctx, "Attempting to join existing session", "session_id", sessionID) tracker, err := clt.GetSessionTracker(ctx, string(*sessionID)) if err != nil { @@ -4351,7 +4361,7 @@ func (h *Handler) validateTrustedCluster(w http.ResponseWriter, r *http.Request, validateResponse, err := h.auth.ValidateTrustedCluster(r.Context(), validateRequest) if err != nil { - h.log.WithError(err).Error("Failed validating trusted cluster") + h.logger.ErrorContext(r.Context(), "Failed validating trusted cluster", "error", err) if trace.IsAccessDenied(err) { return nil, trace.AccessDenied("access denied: the cluster token has been rejected") } @@ -4398,7 +4408,7 @@ func (h *Handler) WithClusterAuth(fn ClusterHandler) httprouter.Handle { }) } -func (h *Handler) writeErrToWebSocket(ws *websocket.Conn, err error) { +func (h *Handler) writeErrToWebSocket(ctx context.Context, ws *websocket.Conn, err error) { if err == nil { return } @@ -4409,11 +4419,11 @@ func (h *Handler) writeErrToWebSocket(ws *websocket.Conn, err error) { } env, err := errEnvelope.Marshal() if err != nil { - h.log.WithError(err).Error("error marshaling proto") + h.logger.ErrorContext(ctx, "error marshaling proto", "error", err) return } if err := ws.WriteMessage(websocket.BinaryMessage, env); err != nil { - h.log.WithError(err).Error("error writing proto") + h.logger.ErrorContext(ctx, "error writing proto", "error", err) return } } @@ -4443,7 +4453,7 @@ func (h *Handler) WithClusterAuthWebSocket(fn ClusterWebsocketHandler) httproute // which should be done by downstream users defer ws.Close() if _, err := fn(w, r, p, sctx, site, ws); err != nil { - h.writeErrToWebSocket(ws, err) + h.writeErrToWebSocket(r.Context(), ws, err) } return nil, nil }) @@ -4460,7 +4470,7 @@ func (h *Handler) authenticateWSRequestWithCluster(w http.ResponseWriter, r *htt return nil, nil, nil, trace.Wrap(err) } - site, err := h.getSiteByParams(sctx, p) + site, err := h.getSiteByParams(r.Context(), sctx, p) if err != nil { return nil, nil, nil, trace.Wrap(err) } @@ -4478,7 +4488,7 @@ func (h *Handler) authenticateRequestWithCluster(w http.ResponseWriter, r *http. return nil, nil, trace.Wrap(err) } - site, err := h.getSiteByParams(sctx, p) + site, err := h.getSiteByParams(r.Context(), sctx, p) if err != nil { return nil, nil, trace.Wrap(err) } @@ -4488,18 +4498,18 @@ func (h *Handler) authenticateRequestWithCluster(w http.ResponseWriter, r *http. // getSiteByParams gets the remoteSite (which can represent this local cluster or a // remote trusted cluster) as specified by the ":site" url parameter. -func (h *Handler) getSiteByParams(sctx *SessionContext, p httprouter.Params) (reversetunnelclient.RemoteSite, error) { +func (h *Handler) getSiteByParams(ctx context.Context, sctx *SessionContext, p httprouter.Params) (reversetunnelclient.RemoteSite, error) { clusterName := p.ByName("site") if clusterName == currentSiteShortcut { res, err := h.cfg.ProxyClient.GetClusterName() if err != nil { - h.log.WithError(err).Warn("Failed to query cluster name.") + h.logger.WarnContext(ctx, "Failed to query cluster name", "error", err) return nil, trace.Wrap(err) } clusterName = res.GetClusterName() } - site, err := h.getSiteByClusterName(sctx, clusterName) + site, err := h.getSiteByClusterName(ctx, sctx, clusterName) if err != nil { return nil, trace.Wrap(err) } @@ -4507,16 +4517,16 @@ func (h *Handler) getSiteByParams(sctx *SessionContext, p httprouter.Params) (re return site, nil } -func (h *Handler) getSiteByClusterName(ctx *SessionContext, clusterName string) (reversetunnelclient.RemoteSite, error) { - proxy, err := h.ProxyWithRoles(ctx) +func (h *Handler) getSiteByClusterName(ctx context.Context, sctx *SessionContext, clusterName string) (reversetunnelclient.RemoteSite, error) { + proxy, err := h.ProxyWithRoles(ctx, sctx) if err != nil { - h.log.WithError(err).Warn("Failed to get proxy with roles.") + h.logger.WarnContext(ctx, "Failed to get proxy with roles", "error", err) return nil, trace.Wrap(err) } site, err := proxy.GetSite(clusterName) if err != nil { - h.log.WithError(err).WithField("cluster-name", clusterName).Warn("Failed to query site.") + h.logger.WarnContext(ctx, "Failed to query site", "error", err, "cluster", clusterName) return nil, trace.Wrap(err) } @@ -4540,7 +4550,7 @@ type clusterClientProvider struct { // UserClientForCluster returns a client to the local or remote cluster // identified by clusterName and is authenticated with the identity of the user. func (r clusterClientProvider) UserClientForCluster(ctx context.Context, clusterName string) (authclient.ClientI, error) { - site, err := r.h.getSiteByClusterName(r.ctx, clusterName) + site, err := r.h.getSiteByClusterName(ctx, r.ctx, clusterName) if err != nil { return nil, trace.Wrap(err) } @@ -4594,7 +4604,7 @@ func (h *Handler) WithProvisionTokenAuth(fn ProvisionTokenHandler) httprouter.Ha site, err := h.cfg.Proxy.GetSite(h.auth.clusterName) if err != nil { - h.log.WithError(err).WithField("cluster-name", h.auth.clusterName).Warn("Failed to query cluster.") + h.logger.WarnContext(ctx, "Failed to query cluster", "error", err, "cluster", h.auth.clusterName) return nil, trace.Wrap(err) } @@ -4675,7 +4685,7 @@ func (h *Handler) WithMetaRedirect(fn redirectHandlerFunc) httprouter.Handle { } err := app.MetaRedirect(w, redirectURL) if err != nil { - h.log.WithError(err).Warn("Failed to issue a redirect.") + h.logger.WarnContext(r.Context(), "Failed to issue a redirect", "error", err) } } } @@ -4903,7 +4913,7 @@ func (h *Handler) AuthenticateRequestWS(w http.ResponseWriter, r *http.Request) Message: "invalid token", }) if writeErr != nil { - log.Errorf("Error while writing invalid token error to websocket: %s", writeErr) + h.logger.ErrorContext(r.Context(), "Error while writing invalid token error to websocket", "error", writeErr) } return nil, nil, trace.Wrap(err) @@ -4930,10 +4940,10 @@ func (h *Handler) AuthenticateRequestWS(w http.ResponseWriter, r *http.Request) // ProxyWithRoles returns a reverse tunnel proxy verifying the permissions // of the given user. -func (h *Handler) ProxyWithRoles(ctx *SessionContext) (reversetunnelclient.Tunnel, error) { - accessChecker, err := ctx.GetUserAccessChecker() +func (h *Handler) ProxyWithRoles(ctx context.Context, sctx *SessionContext) (reversetunnelclient.Tunnel, error) { + accessChecker, err := sctx.GetUserAccessChecker() if err != nil { - h.log.WithError(err).Warn("Failed to get client roles.") + h.logger.WarnContext(ctx, "Failed to get client roles", "error", err) return nil, trace.Wrap(err) } @@ -5138,7 +5148,7 @@ func (h *Handler) authExportPublic(w http.ResponseWriter, r *http.Request, p htt }, ) if err != nil { - h.log.WithError(err).Debug("Failed to generate CA Certs.") + h.logger.DebugContext(r.Context(), "Failed to generate CA Certs", "error", err) http.Error(w, err.Error(), trace.ErrorToCode(err)) return } diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index 48a9ff61179a5..f3a71ebb78945 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -1776,7 +1776,7 @@ func TestNewTerminalHandler(t *testing.T) { require.Equal(t, validCfg.Term, term.term) require.Equal(t, validCfg.DisplayLogin, term.displayLogin) // newly added - require.NotNil(t, term.log) + require.NotNil(t, term.logger) } func TestUIConfig(t *testing.T) { @@ -8209,7 +8209,7 @@ func createProxy(ctx context.Context, t *testing.T, proxyID string, node *regula go func() { if err := mux.Serve(); err != nil && !utils.IsOKNetworkError(err) { - log.WithError(err).Error("Mux encountered err serving") + slog.ErrorContext(context.Background(), "Mux encountered error serving", "error", err) } }() @@ -8283,7 +8283,7 @@ func createProxy(ctx context.Context, t *testing.T, proxyID string, node *regula go func() { if err := sshGRPCServer.Serve(mux.TLS()); err != nil && !utils.IsOKNetworkError(err) { - log.WithError(err).Error("gRPC proxy server terminated unexpectedly") + slog.ErrorContext(context.Background(), "gRPC proxy server terminated unexpectedly", "error", err) } }() @@ -8358,7 +8358,7 @@ func createProxy(ctx context.Context, t *testing.T, proxyID string, node *regula t.Cleanup(webServer.Close) go func() { if err := proxyServer.Serve(mux.SSH()); err != nil && !utils.IsOKNetworkError(err) { - log.WithError(err).Error("SSH proxy server terminated unexpectedly") + slog.ErrorContext(context.Background(), "SSH proxy server terminated unexpectedly", "error", err) } }() @@ -10677,7 +10677,7 @@ func TestWebSocketClosedBeforeSSHSessionCreated(t *testing.T) { // not yet established. stream := terminal.NewStream(ctx, terminal.StreamConfig{ WS: ws, - Logger: utils.NewLogger(), + Logger: utils.NewSlogLoggerForTests(), Handlers: map[string]terminal.WSHandlerFunc{ defaults.WebsocketSessionMetadata: func(ctx context.Context, envelope terminal.Envelope) { if envelope.Type != defaults.WebsocketSessionMetadata { diff --git a/lib/web/apiserver_test_utils.go b/lib/web/apiserver_test_utils.go index e3d1f3c4ba9b1..d7fe5cd0bb3d7 100644 --- a/lib/web/apiserver_test_utils.go +++ b/lib/web/apiserver_test_utils.go @@ -19,6 +19,8 @@ package web import ( + "context" + "log/slog" "net/http" "os" "path/filepath" @@ -38,7 +40,7 @@ func newDebugFileSystem() (http.FileSystem, error) { return nil, trace.Wrap(err) } } - log.Infof("Using filesystem for serving web assets: %s.", assetsPath) + slog.InfoContext(context.TODO(), "Using filesystem for serving web assets", "assets_path", assetsPath) return http.Dir(assetsPath), nil } diff --git a/lib/web/apps.go b/lib/web/apps.go index 5e809d2df29e1..8ae0dc5525468 100644 --- a/lib/web/apps.go +++ b/lib/web/apps.go @@ -69,7 +69,7 @@ func (h *Handler) clusterAppsGet(w http.ResponseWriter, r *http.Request, p httpr UseSearchAsRoles: true, }) if err != nil { - h.log.Debugf("Unable to fetch user groups while listing applications, unable to display associated user groups: %v", err) + h.logger.DebugContext(r.Context(), "Unable to fetch user groups while listing applications, unable to display associated user groups", "error", err) } userGroupLookup := make(map[string]types.UserGroup, len(userGroups)) @@ -94,7 +94,7 @@ func (h *Handler) clusterAppsGet(w http.ResponseWriter, r *http.Request, p httpr if app.IsAWSConsole() { allowedAWSRoles, err := accessChecker.GetAllowedLoginsForResource(app) if err != nil { - h.log.Debugf("Unable to find allowed AWS Roles for app %s, skipping", app.GetName()) + h.logger.DebugContext(r.Context(), "Unable to find allowed AWS Roles for app, skipping", "app", app.GetName()) continue } @@ -105,7 +105,7 @@ func (h *Handler) clusterAppsGet(w http.ResponseWriter, r *http.Request, p httpr for _, userGroupName := range app.GetUserGroups() { userGroup := userGroupLookup[userGroupName] if userGroup == nil { - h.log.Debugf("Unable to find user group %s when creating user groups, skipping", userGroupName) + h.logger.DebugContext(r.Context(), "Unable to find user group when creating user groups, skipping", "user_group", userGroupName) continue } @@ -172,7 +172,7 @@ func (h *Handler) getAppDetails(w http.ResponseWriter, r *http.Request, p httpro for _, required := range requiredAppNames { res, err := h.resolveApp(r.Context(), ctx, ResolveAppParams{ClusterName: clusterName, AppName: required}) if err != nil { - h.log.Errorf("Error getting app details for %s, a required app for %s", required, result.App.GetName()) + h.logger.ErrorContext(r.Context(), "Error getting app details for associated required app", "required_app", required, "app", result.App.GetName()) continue } resp.RequiredAppFQDNs = append(resp.RequiredAppFQDNs, res.App.GetPublicAddr()) @@ -218,12 +218,12 @@ func (h *Handler) createAppSession(w http.ResponseWriter, r *http.Request, p htt return nil, trace.Wrap(err, "unable to resolve FQDN: %v", req.FQDNHint) } - h.log.Debugf("Creating application web session for %v in %v.", result.App.GetPublicAddr(), result.ClusterName) + h.logger.DebugContext(r.Context(), "Creating application web session", "app_public_addr", result.App.GetPublicAddr(), "cluster", result.ClusterName) // Ensuring proxy can handle the connection is only done when the request is // coming from the WebUI. if h.healthCheckAppServer != nil && !app.HasClientCert(r) { - h.log.Debugf("Ensuring proxy can handle requests requests for application %q.", result.App.GetName()) + h.logger.DebugContext(r.Context(), "Ensuring proxy can handle requests requests for application", "app", result.App.GetName()) err := h.healthCheckAppServer(r.Context(), result.App.GetPublicAddr(), result.ClusterName) if err != nil { return nil, trace.ConnectionProblem(err, "Unable to serve application requests. Please try again. If the issue persists, verify if the Application Services are connected to Teleport.") @@ -315,7 +315,7 @@ func (h *Handler) resolveApp(ctx context.Context, scx *SessionContext, params Re } // Get a reverse tunnel proxy aware of the user's permissions. - proxy, err := h.ProxyWithRoles(scx) + proxy, err := h.ProxyWithRoles(ctx, scx) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/web/autoupdate_rfd109.go b/lib/web/autoupdate_rfd109.go index 3bbdd0175b106..7b2a438e7f477 100644 --- a/lib/web/autoupdate_rfd109.go +++ b/lib/web/autoupdate_rfd109.go @@ -62,10 +62,10 @@ func (h *Handler) automaticUpgrades109(w http.ResponseWriter, r *http.Request, p // Finally, we treat the request based on its type switch requestType { case "version": - h.log.Debugf("Agent requesting version for channel %s", channelName) + h.logger.DebugContext(r.Context(), "Agent requesting version for channel", "channel", channelName) return h.automaticUpgradesVersion109(w, r, channelName) case "critical": - h.log.Debugf("Agent requesting criticality for channel %s", channelName) + h.logger.DebugContext(r.Context(), "Agent requesting criticality for channel", "channel", channelName) return h.automaticUpgradesCritical109(w, r, channelName) default: return nil, trace.BadParameter("requestType path must end with 'version' or 'critical'") diff --git a/lib/web/desktop.go b/lib/web/desktop.go index b72d4108b5ef8..63a6bdee24508 100644 --- a/lib/web/desktop.go +++ b/lib/web/desktop.go @@ -25,6 +25,7 @@ import ( "crypto/tls" "errors" "io" + "log/slog" "math/rand/v2" "net" "net/http" @@ -33,7 +34,6 @@ import ( "github.com/gorilla/websocket" "github.com/gravitational/trace" "github.com/julienschmidt/httprouter" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/constants" @@ -48,6 +48,7 @@ import ( "github.com/gravitational/teleport/lib/reversetunnelclient" "github.com/gravitational/teleport/lib/srv/desktop/tdp" "github.com/gravitational/teleport/lib/utils" + logutils "github.com/gravitational/teleport/lib/utils/log" ) // GET /webapi/sites/:site/desktops/:desktopName/connect?access_token=&username= @@ -64,15 +65,18 @@ func (h *Handler) desktopConnectHandle( return nil, trace.BadParameter("missing desktopName in request URL") } - log := sctx.cfg.Log.WithField("desktop-name", desktopName).WithField("cluster-name", site.GetName()) - log.Debug("New desktop access websocket connection") + log := sctx.cfg.Log.With( + "desktop_name", desktopName, + "cluster_name", site.GetName(), + ) + log.DebugContext(r.Context(), "New desktop access websocket connection") if err := h.createDesktopConnection(r, desktopName, site.GetName(), log, sctx, site, ws); err != nil { // createDesktopConnection makes a best effort attempt to send an error to the user // (via websocket) before terminating the connection. We log the error here, but // return nil because our HTTP middleware will try to write the returned error in JSON // format, and this will fail since the HTTP connection has been upgraded to websockets. - log.Error(err) + log.ErrorContext(r.Context(), "creating desktop connection failed", "error", err) } return nil, nil @@ -82,7 +86,7 @@ func (h *Handler) createDesktopConnection( r *http.Request, desktopName string, clusterName string, - log *logrus.Entry, + log *slog.Logger, sctx *SessionContext, site reversetunnelclient.RemoteSite, ws *websocket.Conn, @@ -102,7 +106,7 @@ func (h *Handler) createDesktopConnection( if err != nil { return sendTDPError(err) } - log.Debugf("Attempting to connect to desktop using username=%v\n", username) + log.DebugContext(ctx, "Attempting to connect to desktop", "username", username) // Read the tdp.ClientScreenSpec from the websocket. // This is always the first thing sent by the client. @@ -123,7 +127,11 @@ func (h *Handler) createDesktopConnection( )) } - log.Debugf("Attempting to connect to desktop using username=%v, width=%v, height=%v\n", username, width, height) + log.DebugContext(ctx, "Attempting to connect to desktop", + "username", username, + "width", width, + "height", height, + ) // Pick a random Windows desktop service as our gateway. // When agent mode is implemented in the service, we'll have to filter out @@ -190,7 +198,7 @@ func (h *Handler) createDesktopConnection( clientSrcAddr: clientSrcAddr, clientDstAddr: clientDstAddr, } - serviceConn, _, err := c.connectToWindowsService(clusterName, validServiceIDs) + serviceConn, _, err := c.connectToWindowsService(ctx, clusterName, validServiceIDs) if err != nil { return sendTDPError(trace.Wrap(err, "cannot connect to Windows Desktop Service")) } @@ -201,7 +209,7 @@ func (h *Handler) createDesktopConnection( if err := serviceConnTLS.HandshakeContext(ctx); err != nil { return sendTDPError(err) } - log.Debug("Connected to windows_desktop_service") + log.DebugContext(ctx, "Connected to windows_desktop_service") tdpConn := tdp.NewConn(serviceConnTLS) @@ -217,7 +225,7 @@ func (h *Handler) createDesktopConnection( return sendTDPError(err) } for _, msg := range withheld { - log.Debugf("Sending withheld message: %v", msg) + log.DebugContext(ctx, "Sending withheld message", "messgage", logutils.TypeAttr(msg)) if err := tdpConn.WriteMessage(msg); err != nil { return sendTDPError(err) } @@ -228,7 +236,10 @@ func (h *Handler) createDesktopConnection( // proxyWebsocketConn hangs here until connection is closed handleProxyWebsocketConnErr( - proxyWebsocketConn(ws, serviceConnTLS), log) + ctx, + proxyWebsocketConn(ws, serviceConnTLS), + log, + ) return nil } @@ -410,7 +421,7 @@ func (h *Handler) performSessionMFACeremony( if tdp.MessageType(buf[0]) != tdp.TypeMFA { // This is not an MFA message, withhold it for later. msg, err := tdp.Decode(buf) - h.log.Debugf("Received non-MFA message, withholding:", msg) + h.logger.DebugContext(ctx, "Received non-MFA message, withholding", "msg_type", logutils.TypeAttr(msg)) if err != nil { return nil, trace.Wrap(err) } @@ -465,7 +476,7 @@ func readClientScreenSpec(ws *websocket.Conn) (*tdp.ClientScreenSpec, error) { } type connector struct { - log *logrus.Entry + log *slog.Logger clt authclient.ClientI site reversetunnelclient.RemoteSite clientSrcAddr net.Addr @@ -476,17 +487,21 @@ type connector struct { // by trying each of the services provided. It returns an error if it could not connect // to any of the services or if it encounters an error that is not a connection problem. func (c *connector) connectToWindowsService( + ctx context.Context, clusterName string, desktopServiceIDs []string, ) (conn net.Conn, version string, err error) { for _, id := range desktopServiceIDs { - conn, ver, err := c.tryConnect(clusterName, id) + conn, ver, err := c.tryConnect(ctx, clusterName, id) if err != nil && !trace.IsConnectionProblem(err) { return nil, "", trace.WrapWithMessage(err, "error connecting to windows_desktop_service %q", id) } if trace.IsConnectionProblem(err) { - c.log.Warnf("failed to connect to windows_desktop_service %q: %v", id, err) + c.log.WarnContext(ctx, "failed to connect to windows_desktop_service", + "windows_desktop_service_id", id, + "error", err, + ) continue } if err == nil { @@ -496,17 +511,19 @@ func (c *connector) connectToWindowsService( return nil, "", trace.Errorf("failed to connect to any windows_desktop_service") } -func (c *connector) tryConnect(clusterName, desktopServiceID string) (conn net.Conn, version string, err error) { - service, err := c.clt.GetWindowsDesktopService(context.Background(), desktopServiceID) +func (c *connector) tryConnect(ctx context.Context, clusterName, desktopServiceID string) (conn net.Conn, version string, err error) { + service, err := c.clt.GetWindowsDesktopService(ctx, desktopServiceID) if err != nil { - log.Errorf("Error finding service with id %s", desktopServiceID) + c.log.ErrorContext(ctx, "Error finding service", "service_id", desktopServiceID, "error", err) return nil, "", trace.NotFound("could not find windows desktop service %s: %v", desktopServiceID, err) } ver := service.GetTeleportVersion() - *c.log = *c.log.WithField("windows-service-version", ver) - *c.log = *c.log.WithField("windows-service-uuid", service.GetName()) - *c.log = *c.log.WithField("windows-service-addr", service.GetAddr()) + *c.log = *c.log.With( + "windows_service_version", ver, + "windows_service_uuid", service.GetName(), + "windows_service_addr", service.GetAddr(), + ) conn, err = c.site.DialTCP(reversetunnelclient.DialParams{ From: c.clientSrcAddr, @@ -625,9 +642,9 @@ func proxyWebsocketConn(ws *websocket.Conn, wds net.Conn) error { // handleProxyWebsocketConnErr handles the error returned by proxyWebsocketConn by // unwrapping it and determining whether to log an error. -func handleProxyWebsocketConnErr(proxyWsConnErr error, log *logrus.Entry) { +func handleProxyWebsocketConnErr(ctx context.Context, proxyWsConnErr error, log *slog.Logger) { if proxyWsConnErr == nil { - log.Debug("proxyWebsocketConn returned with no error") + log.DebugContext(ctx, "proxyWebsocketConn returned with no error") return } @@ -645,7 +662,7 @@ func handleProxyWebsocketConnErr(proxyWsConnErr error, log *logrus.Entry) { switch closeErr.Code { case websocket.CloseNormalClosure, // when the user hits "disconnect" from the menu websocket.CloseGoingAway: // when the user closes the tab - log.Debugf("Web socket closed by client with code: %v", closeErr.Code) + log.DebugContext(ctx, "Web socket closed by client", "close_code", closeErr.Code) return } return @@ -656,7 +673,7 @@ func handleProxyWebsocketConnErr(proxyWsConnErr error, log *logrus.Entry) { } } - log.WithError(proxyWsConnErr).Warning("Error proxying a desktop protocol websocket to windows_desktop_service") + log.WarnContext(ctx, "Error proxying a desktop protocol websocket to windows_desktop_service", "error", proxyWsConnErr) } // sendTDPAlert sends a tdp Notification over the supplied websocket with the diff --git a/lib/web/desktop/playback.go b/lib/web/desktop/playback.go index 38fcb8c38b0b9..db283af906c7d 100644 --- a/lib/web/desktop/playback.go +++ b/lib/web/desktop/playback.go @@ -22,10 +22,10 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "time" "github.com/gorilla/websocket" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/lib/player" @@ -65,7 +65,8 @@ type actionMessage struct { // ReceivePlaybackActions handles logic for receiving playbackAction messages // over the websocket and updating the player state accordingly. func ReceivePlaybackActions( - log logrus.FieldLogger, + ctx context.Context, + logger *slog.Logger, ws *websocket.Conn, player *player.Player) { // playback always starts in a playing state @@ -78,7 +79,7 @@ func ReceivePlaybackActions( // Connection close errors are expected if the user closes the tab. // Only log unexpected errors to avoid cluttering the logs. if !utils.IsOKNetworkError(err) { - log.Warnf("websocket read error: %v", err) + logger.WarnContext(ctx, "websocket read error", "error", err) } return } @@ -98,7 +99,7 @@ func ReceivePlaybackActions( case actionSeek: player.SetPos(time.Duration(action.Pos) * time.Millisecond) default: - log.Warnf("invalid desktop playback action: %v", action.Action) + slog.WarnContext(ctx, "invalid desktop playback action", "action", action.Action) return } } @@ -108,7 +109,7 @@ func ReceivePlaybackActions( // over a websocket. func PlayRecording( ctx context.Context, - log logrus.FieldLogger, + log *slog.Logger, ws *websocket.Conn, player *player.Player) { player.Play() @@ -122,18 +123,18 @@ func PlayRecording( // Attempt to JSONify the error (escaping any quotes) msg, err := json.Marshal(playerErr.Error()) if err != nil { - log.Warnf("failed to marshal player error message: %v", err) + log.WarnContext(ctx, "failed to marshal player error message", "error", err) msg = []byte(`"internal server error"`) } //lint:ignore QF1012 this write needs to happen in a single operation bytes := []byte(fmt.Sprintf(`{"message":"error", "errorText":%s}`, string(msg))) if err := ws.WriteMessage(websocket.BinaryMessage, bytes); err != nil { - log.Errorf("failed to write error message: %v", err) + log.ErrorContext(ctx, "failed to write error message", "error", err) } return } if err := ws.WriteMessage(websocket.BinaryMessage, []byte(`{"message":"end"}`)); err != nil { - log.Errorf("failed to write end message: %v", err) + log.ErrorContext(ctx, "failed to write end message", "error", err) } return } @@ -145,7 +146,7 @@ func PlayRecording( } msg, err := utils.FastMarshal(evt) if err != nil { - log.Errorf("failed to marshal desktop event: %v", err) + log.ErrorContext(ctx, "failed to marshal desktop event", "error", err) ws.WriteMessage(websocket.BinaryMessage, []byte(`{"message":"error","errorText":"server error"}`)) return } @@ -153,7 +154,7 @@ func PlayRecording( // Connection close errors are expected if the user closes the tab. // Only log unexpected errors to avoid cluttering the logs. if !utils.IsOKNetworkError(err) { - log.Warnf("websocket write error: %v", err) + log.WarnContext(ctx, "websocket write error", "error", err) } return } diff --git a/lib/web/desktop/playback_test.go b/lib/web/desktop/playback_test.go index 12c52ad7e2862..da284e4d18792 100644 --- a/lib/web/desktop/playback_test.go +++ b/lib/web/desktop/playback_test.go @@ -82,7 +82,6 @@ func newServer(t *testing.T, streamInterval time.Duration, events []apievents.Au fs := eventstest.NewFakeStreamer(events, streamInterval) log := utils.NewSlogLoggerForTests() - logrusLogger := utils.NewLoggerForTests() s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upgrader := websocket.Upgrader{ @@ -103,7 +102,7 @@ func newServer(t *testing.T, streamInterval time.Duration, events []apievents.Au }) assert.NoError(t, err) player.Play() - desktop.PlayRecording(r.Context(), logrusLogger, ws, player) + desktop.PlayRecording(r.Context(), log, ws, player) })) t.Cleanup(s.Close) diff --git a/lib/web/desktop_playback.go b/lib/web/desktop_playback.go index 1467c14a28165..37611058a8131 100644 --- a/lib/web/desktop_playback.go +++ b/lib/web/desktop_playback.go @@ -58,7 +58,7 @@ func (h *Handler) desktopPlaybackHandle( Context: r.Context(), }) if err != nil { - h.log.Errorf("couldn't create player for session %v: %v", sID, err) + h.logger.ErrorContext(r.Context(), "couldn't create player for session", "session_id", sID, "error", err) ws.WriteMessage(websocket.BinaryMessage, []byte(`{"message": "error", "errorText": "Internal server error"}`)) return nil, nil @@ -71,13 +71,13 @@ func (h *Handler) desktopPlaybackHandle( go func() { defer cancel() - desktop.ReceivePlaybackActions(h.log, ws, player) + desktop.ReceivePlaybackActions(ctx, h.logger, ws, player) }() go func() { defer cancel() defer ws.Close() - desktop.PlayRecording(ctx, h.log, ws, player) + desktop.PlayRecording(ctx, h.logger, ws, player) }() <-ctx.Done() diff --git a/lib/web/device_trust.go b/lib/web/device_trust.go index dce54528bdcc7..1739ff583faf0 100644 --- a/lib/web/device_trust.go +++ b/lib/web/device_trust.go @@ -66,17 +66,17 @@ func (h *Handler) deviceWebConfirm(w http.ResponseWriter, r *http.Request, _ htt }) switch { case err != nil: - h.log. - WithError(err). - WithField("user", sessionCtx.GetUser()). - Warn("Device web authentication confirm failed") + h.logger.WarnContext(ctx, "Device web authentication confirm failed", + "error", err, + "user", sessionCtx.GetUser(), + ) // err swallowed on purpose. default: // Preemptively release session from cache, as its certificates are now // updated. // The WebSession watcher takes care of this in other proxy instances // (see [sessionCache.watchWebSessions]). - h.auth.releaseResources(sessionCtx.GetUser(), sessionCtx.GetSessionID()) + h.auth.releaseResources(r.Context(), sessionCtx.GetUser(), sessionCtx.GetSessionID()) } // Always redirect back to the dashboard, regardless of outcome. @@ -84,10 +84,10 @@ func (h *Handler) deviceWebConfirm(w http.ResponseWriter, r *http.Request, _ htt redirectTo, err := h.getRedirectPath(unsafeRedirectURI) if err != nil { - h.log. - WithError(err). - WithField("redirect_uri", unsafeRedirectURI). - Debug("Unable to parse redirectURI") + h.logger.DebugContext(ctx, "Unable to parse redirectURI", + "error", err, + "redirect_uri", unsafeRedirectURI, + ) } http.Redirect(w, r, redirectTo, http.StatusSeeOther) diff --git a/lib/web/features.go b/lib/web/features.go index 7eeb70eb9f060..a05085417e6f9 100644 --- a/lib/web/features.go +++ b/lib/web/features.go @@ -33,7 +33,7 @@ func (h *Handler) SetClusterFeatures(features proto.Features) { defer h.Mutex.Unlock() if !bytes.Equal(h.clusterFeatures.CloudAnonymizationKey, features.CloudAnonymizationKey) { - h.log.Info("Received new cloud anonymization key from server") + h.logger.InfoContext(h.cfg.Context, "Received new cloud anonymization key from server") } entitlements.BackfillFeatures(&features) @@ -54,25 +54,25 @@ func (h *Handler) GetClusterFeatures() proto.Features { // The watcher doesn't ping the auth server immediately upon start because features are // already set by the config object in `NewHandler`. func (h *Handler) startFeatureWatcher() { - ticker := h.clock.NewTicker(h.cfg.FeatureWatchInterval) - h.log.WithField("interval", h.cfg.FeatureWatchInterval).Info("Proxy handler features watcher has started") ctx := h.cfg.Context + ticker := h.clock.NewTicker(h.cfg.FeatureWatchInterval) + h.logger.InfoContext(ctx, "Proxy handler features watcher has started", "interval", h.cfg.FeatureWatchInterval) defer ticker.Stop() for { select { case <-ticker.Chan(): - h.log.Info("Pinging auth server for features") + h.logger.InfoContext(ctx, "Pinging auth server for features") pingResponse, err := h.GetProxyClient().Ping(ctx) if err != nil { - h.log.WithError(err).Error("Auth server ping failed") + h.logger.ErrorContext(ctx, "Auth server ping failed", "error", err) continue } h.SetClusterFeatures(*pingResponse.ServerFeatures) - h.log.WithField("features", pingResponse.ServerFeatures).Info("Done updating proxy features") + h.logger.InfoContext(ctx, "Done updating proxy features", "features", pingResponse.ServerFeatures) case <-ctx.Done(): - h.log.Info("Feature service has stopped") + h.logger.InfoContext(ctx, "Feature service has stopped") return } } diff --git a/lib/web/features_test.go b/lib/web/features_test.go index 3798e819b46eb..52fcbddc91d0a 100644 --- a/lib/web/features_test.go +++ b/lib/web/features_test.go @@ -71,7 +71,6 @@ func TestFeaturesWatcher(t *testing.T) { }, clock: clock, clusterFeatures: proto.Features{}, - log: newPackageLogger(), logger: slog.Default().With(teleport.ComponentKey, teleport.ComponentWeb), } diff --git a/lib/web/headless.go b/lib/web/headless.go index 882bc1f374d3c..4542a4dd0522b 100644 --- a/lib/web/headless.go +++ b/lib/web/headless.go @@ -48,7 +48,7 @@ func (h *Handler) getHeadless(_ http.ResponseWriter, r *http.Request, params htt if err != nil { // Log the error, but return something more user-friendly. // Context exceeded or invalid request states are more confusing than helpful. - h.log.Debug("failed to get headless session: %v", err) + h.logger.DebugContext(r.Context(), "failed to get headless session", "error", err) return nil, trace.BadParameter("requested invalid headless session") } diff --git a/lib/web/integrations_awsoidc.go b/lib/web/integrations_awsoidc.go index 0597faa8e5425..44444f191c149 100644 --- a/lib/web/integrations_awsoidc.go +++ b/lib/web/integrations_awsoidc.go @@ -1035,7 +1035,7 @@ func (h *Handler) awsOIDCCreateAWSAppAccess(w http.ResponseWriter, r *http.Reque AppClusterName: site.GetName(), AllowedAWSRolesLookup: allowedAWSRolesLookup, UserGroupLookup: getUserGroupLookup(), - Logger: h.log, + Logger: h.logger, }), nil } diff --git a/lib/web/join_tokens.go b/lib/web/join_tokens.go index 16da9906cbd2f..033040a8545e0 100644 --- a/lib/web/join_tokens.go +++ b/lib/web/join_tokens.go @@ -382,7 +382,7 @@ func (h *Handler) getAutoUpgrades(ctx context.Context) (bool, string, error) { if autoUpgrades { autoUpgradesVersion, err = h.cfg.AutomaticUpgradesChannels.DefaultVersion(ctx) if err != nil { - log.WithError(err).Info("Failed to get auto upgrades version.") + h.logger.InfoContext(ctx, "Failed to get auto upgrades version", "error", err) return false, "", trace.Wrap(err) } } @@ -409,14 +409,14 @@ func (h *Handler) getNodeJoinScriptHandle(w http.ResponseWriter, r *http.Request script, err := getJoinScript(r.Context(), settings, h.GetProxyClient()) if err != nil { - log.WithError(err).Info("Failed to return the node install script.") + h.logger.InfoContext(r.Context(), "Failed to return the node install script", "error", err) w.Write(scripts.ErrorBashScript) return nil, nil } w.WriteHeader(http.StatusOK) if _, err := fmt.Fprintln(w, script); err != nil { - log.WithError(err).Info("Failed to return the node install script.") + h.logger.InfoContext(r.Context(), "Failed to return the node install script", "error", err) w.Write(scripts.ErrorBashScript) } @@ -429,14 +429,20 @@ func (h *Handler) getAppJoinScriptHandle(w http.ResponseWriter, r *http.Request, name, err := url.QueryUnescape(queryValues.Get("name")) if err != nil { - log.WithField("query-param", "name").WithError(err).Debug("Failed to return the app install script.") + h.logger.DebugContext(r.Context(), "Failed to return the app install script", + "query_param", "name", + "error", err, + ) w.Write(scripts.ErrorBashScript) return nil, nil } uri, err := url.QueryUnescape(queryValues.Get("uri")) if err != nil { - log.WithField("query-param", "uri").WithError(err).Debug("Failed to return the app install script.") + h.logger.DebugContext(r.Context(), "Failed to return the app install script", + "query_param", "uri", + "error", err, + ) w.Write(scripts.ErrorBashScript) return nil, nil } @@ -458,14 +464,14 @@ func (h *Handler) getAppJoinScriptHandle(w http.ResponseWriter, r *http.Request, script, err := getJoinScript(r.Context(), settings, h.GetProxyClient()) if err != nil { - log.WithError(err).Info("Failed to return the app install script.") + h.logger.InfoContext(r.Context(), "Failed to return the app install script", "error", err) w.Write(scripts.ErrorBashScript) return nil, nil } w.WriteHeader(http.StatusOK) if _, err := fmt.Fprintln(w, script); err != nil { - log.WithError(err).Debug("Failed to return the app install script.") + h.logger.DebugContext(r.Context(), "Failed to return the app install script", "error", err) w.Write(scripts.ErrorBashScript) } @@ -490,14 +496,14 @@ func (h *Handler) getDatabaseJoinScriptHandle(w http.ResponseWriter, r *http.Req script, err := getJoinScript(r.Context(), settings, h.GetProxyClient()) if err != nil { - log.WithError(err).Info("Failed to return the database install script.") + h.logger.InfoContext(r.Context(), "Failed to return the database install script", "error", err) w.Write(scripts.ErrorBashScript) return nil, nil } w.WriteHeader(http.StatusOK) if _, err := fmt.Fprintln(w, script); err != nil { - log.WithError(err).Debug("Failed to return the database install script.") + h.logger.DebugContext(r.Context(), "Failed to return the database install script", "error", err) w.Write(scripts.ErrorBashScript) } @@ -517,12 +523,17 @@ func (h *Handler) getDiscoveryJoinScriptHandle(w http.ResponseWriter, r *http.Re discoveryGroup, err := url.QueryUnescape(queryValues.Get(discoveryGroupQueryParam)) if err != nil { - log.WithField("query-param", discoveryGroupQueryParam).WithError(err).Debug("Failed to return the discovery install script.") + h.logger.DebugContext(r.Context(), "Failed to return the discovery install script", + "error", err, + "query_param", discoveryGroupQueryParam, + ) w.Write(scripts.ErrorBashScript) return nil, nil } if discoveryGroup == "" { - log.WithField("query-param", discoveryGroupQueryParam).Debug("Failed to return the discovery install script. Missing required fields.") + h.logger.DebugContext(r.Context(), "Failed to return the discovery install script. Missing required fields", + "query_param", discoveryGroupQueryParam, + ) w.Write(scripts.ErrorBashScript) return nil, nil } @@ -537,14 +548,14 @@ func (h *Handler) getDiscoveryJoinScriptHandle(w http.ResponseWriter, r *http.Re script, err := getJoinScript(r.Context(), settings, h.GetProxyClient()) if err != nil { - log.WithError(err).Info("Failed to return the discovery install script.") + h.logger.InfoContext(r.Context(), "Failed to return the discovery install script", "error", err) w.Write(scripts.ErrorBashScript) return nil, nil } w.WriteHeader(http.StatusOK) if _, err := fmt.Fprintln(w, script); err != nil { - log.WithError(err).Debug("Failed to return the discovery install script.") + h.logger.DebugContext(r.Context(), "Failed to return the discovery install script", "error", err) w.Write(scripts.ErrorBashScript) } diff --git a/lib/web/kube.go b/lib/web/kube.go index ccfd76380103f..195c6674d92ad 100644 --- a/lib/web/kube.go +++ b/lib/web/kube.go @@ -32,7 +32,6 @@ import ( "github.com/gogo/protobuf/proto" "github.com/gorilla/websocket" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" oteltrace "go.opentelemetry.io/otel/trace" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" @@ -49,6 +48,7 @@ import ( "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/session" + logutils "github.com/gravitational/teleport/lib/utils/log" "github.com/gravitational/teleport/lib/web/terminal" ) @@ -62,7 +62,6 @@ type podHandler struct { sctx *SessionContext ws *websocket.Conn keepAliveInterval time.Duration - log *logrus.Entry logger *slog.Logger userClient authclient.ClientI localCA types.CertAuthority @@ -130,7 +129,10 @@ func (p *podHandler) ServeHTTP(_ http.ResponseWriter, r *http.Request) { sessionMetadataResponse, err := json.Marshal(siteSessionGenerateResponse{Session: p.sess}) if err != nil { - p.sendAndLogError(err) + p.logger.ErrorContext(r.Context(), "failed marshaling session data", "error", err) + if err := p.sendErrorMessage(err); err != nil { + p.logger.ErrorContext(r.Context(), "failed to send error message to client", "error", err) + } return } @@ -142,18 +144,29 @@ func (p *podHandler) ServeHTTP(_ http.ResponseWriter, r *http.Request) { envelopeBytes, err := proto.Marshal(envelope) if err != nil { - p.sendAndLogError(err) + p.logger.ErrorContext(r.Context(), "failed marshaling message envelope", "error", err) + if err := p.sendErrorMessage(err); err != nil { + p.logger.ErrorContext(r.Context(), "failed to send error message to client", "error", err) + } + return } err = p.ws.WriteMessage(websocket.BinaryMessage, envelopeBytes) if err != nil { - p.sendAndLogError(err) + p.logger.ErrorContext(r.Context(), "failed write session data message", "error", err) + if err := p.sendErrorMessage(err); err != nil { + p.logger.ErrorContext(r.Context(), "failed to send error message to client", "error", err) + } + return } if err := p.handler(r); err != nil { - p.sendAndLogError(err) + p.logger.ErrorContext(r.Context(), "handling kube session unexpectedly terminated", "error", err) + if err := p.sendErrorMessage(err); err != nil { + p.logger.ErrorContext(r.Context(), "failed to send error message to client", "error", err) + } } } @@ -161,11 +174,9 @@ func (p *podHandler) Close() error { return trace.Wrap(p.ws.Close()) } -func (p *podHandler) sendAndLogError(err error) { - p.log.Error(err) - +func (p *podHandler) sendErrorMessage(err error) error { if p.closedByClient.Load() { - return + return nil } envelope := &terminal.Envelope{ @@ -176,16 +187,17 @@ func (p *podHandler) sendAndLogError(err error) { envelopeBytes, err := proto.Marshal(envelope) if err != nil { - p.log.WithError(err).Error("failed to marshal error message") - return + return trace.Wrap(err, "creating envelope payload") } if err := p.ws.WriteMessage(websocket.BinaryMessage, envelopeBytes); err != nil { - p.log.WithError(err).Error("failed to send error message") + return trace.Wrap(err, "writing error message") } + + return nil } func (p *podHandler) handler(r *http.Request) error { - p.log.Debug("Creating websocket stream for a kube exec request") + p.logger.DebugContext(r.Context(), "Creating websocket stream for a kube exec request") // Create a context for signaling when the terminal session is over and // link it first with the trace context from the request context @@ -196,7 +208,7 @@ func (p *podHandler) handler(r *http.Request) error { defaultCloseHandler := p.ws.CloseHandler() p.ws.SetCloseHandler(func(code int, text string) error { p.closedByClient.Store(true) - p.log.Debug("websocket connection was closed by client") + p.logger.DebugContext(r.Context(), "websocket connection was closed by client") cancel() // Call the default close handler if one was set. @@ -229,7 +241,13 @@ func (p *podHandler) handler(r *http.Request) error { Width: p.req.Term.Winsize().Width, Height: p.req.Term.Winsize().Height, }) - stream := terminal.NewStream(ctx, terminal.StreamConfig{WS: p.ws, Logger: p.log, Handlers: map[string]terminal.WSHandlerFunc{defaults.WebsocketResize: p.handleResize(resizeQueue)}}) + stream := terminal.NewStream(ctx, terminal.StreamConfig{ + WS: p.ws, + Logger: p.logger, + Handlers: map[string]terminal.WSHandlerFunc{ + defaults.WebsocketResize: p.handleResize(resizeQueue), + }, + }) certsReq := clientproto.UserCertsRequest{ TLSPublicKey: publicKeyPEM, @@ -284,7 +302,7 @@ func (p *podHandler) handler(r *http.Request) error { } kubeReq.VersionedParams(option, scheme.ParameterCodec) - p.log.Debugf("Web kube exec request URL: %s", kubeReq.URL()) + p.logger.DebugContext(ctx, "Web kube exec request created", "url", logutils.StringerAttr(kubeReq.URL())) wsExec, err := remotecommand.NewWebSocketExecutor(restConfig, "POST", kubeReq.URL().String()) if err != nil { @@ -315,16 +333,16 @@ func (p *podHandler) handler(r *http.Request) error { if p.req.IsInteractive { // Send close envelope to web terminal upon exit without an error. if err := stream.SendCloseMessage(""); err != nil { - p.log.WithError(err).Error("unable to send close event to web client.") + p.logger.ErrorContext(ctx, "unable to send close event to web client", "error", err) } } if err := stream.Close(); err != nil { - p.log.WithError(err).Error("unable to close websocket stream to web client.") + p.logger.ErrorContext(ctx, "unable to close websocket stream to web client", "error", err) return nil } - p.log.Debug("Sent close event to web client.") + p.logger.DebugContext(ctx, "Sent close event to web client", "error", err) return nil } @@ -333,19 +351,19 @@ func (p *podHandler) handleResize(termSizeQueue *termSizeQueue) func(context.Con return func(ctx context.Context, envelope terminal.Envelope) { var e map[string]any if err := json.Unmarshal([]byte(envelope.Payload), &e); err != nil { - p.log.Warnf("Failed to parse resize payload: %v", err) + p.logger.WarnContext(ctx, "Failed to parse resize payload", "error", err) return } size, ok := e["size"].(string) if !ok { - p.log.Errorf("expected size to be of type string, got type %T instead", size) + p.logger.ErrorContext(ctx, "got unexpected size type, expected type string", "size_type", logutils.TypeAttr(size)) return } params, err := session.UnmarshalTerminalParams(size) if err != nil { - p.log.Warnf("Failed to retrieve terminal size: %v", err) + p.logger.WarnContext(ctx, "Failed to retrieve terminal size", "error", err) return } diff --git a/lib/web/server.go b/lib/web/server.go index 0af94831d30e6..a1f113cd53995 100644 --- a/lib/web/server.go +++ b/lib/web/server.go @@ -20,17 +20,16 @@ package web import ( "context" + "log/slog" "net" "net/http" "sync" "time" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/defaults" - "github.com/gravitational/teleport/lib/utils" ) // ServerConfig provides dependencies required to create a [Server]. @@ -40,7 +39,7 @@ type ServerConfig struct { // Handler web handler Handler *APIHandler // Log to write log messages - Log logrus.FieldLogger + Log *slog.Logger // ShutdownPollPeriod sets polling period for shutdown ShutdownPollPeriod time.Duration } @@ -60,7 +59,7 @@ func (c *ServerConfig) CheckAndSetDefaults() error { } if c.Log == nil { - c.Log = utils.NewLogger().WithField(teleport.ComponentKey, teleport.ComponentProxy) + c.Log = slog.With(teleport.ComponentKey, teleport.ComponentProxy) } return nil @@ -138,7 +137,7 @@ func (s *Server) Shutdown(ctx context.Context) error { return trace.NewAggregate(err, s.cfg.Handler.Close()) } - s.cfg.Log.Infof("Shutdown: waiting for %v connections to finish.", activeConnections) + s.cfg.Log.InfoContext(ctx, "Shutdown: waiting for active connections to finish", "active_connection_count", activeConnections) lastReport := time.Time{} ticker := time.NewTicker(s.cfg.ShutdownPollPeriod) defer ticker.Stop() @@ -151,11 +150,11 @@ func (s *Server) Shutdown(ctx context.Context) error { return trace.NewAggregate(err, s.cfg.Handler.Close()) } if time.Since(lastReport) > 10*s.cfg.ShutdownPollPeriod { - s.cfg.Log.Infof("Shutdown: waiting for %v connections to finish.", activeConnections) + s.cfg.Log.InfoContext(ctx, "Shutdown: waiting for active connections to finish", "active_connection_count", activeConnections) lastReport = time.Now() } case <-ctx.Done(): - s.cfg.Log.Infof("Context canceled wait, returning.") + s.cfg.Log.InfoContext(ctx, "Context canceled wait, returning") return trace.ConnectionProblem(trace.NewAggregate(err, s.cfg.Handler.Close()), "context canceled") } } diff --git a/lib/web/sessions.go b/lib/web/sessions.go index b429920d45708..67f21be47db5c 100644 --- a/lib/web/sessions.go +++ b/lib/web/sessions.go @@ -25,6 +25,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net" "sync" "sync/atomic" @@ -32,7 +33,6 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" @@ -62,6 +62,7 @@ import ( "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" + logutils "github.com/gravitational/teleport/lib/utils/log" ) // SessionContext is a context associated with a user's @@ -86,7 +87,7 @@ type SessionContext struct { type SessionContextConfig struct { // Log is used to emit logs - Log *logrus.Entry + Log *slog.Logger // User is the name of the current user User string @@ -139,10 +140,10 @@ func (c *SessionContextConfig) CheckAndSetDefaults() error { } if c.Log == nil { - c.Log = log.WithFields(logrus.Fields{ - "user": c.User, - "session": c.Session.GetShortName(), - }) + c.Log = slog.With( + "user", c.User, + "session", c.Session.GetShortName(), + ) } if c.newRemoteClient == nil { @@ -202,8 +203,12 @@ func (c *SessionContext) validateBearerToken(ctx context.Context, token string) } if fetchedToken.GetUser() != c.cfg.User { - c.cfg.Log.Warnf("Failed validating bearer token: the user[%s] in bearer token[%s] did not match the user[%s] for session[%s]", - fetchedToken.GetUser(), token, c.cfg.User, c.GetSessionID()) + c.cfg.Log.WarnContext(ctx, "Failed validating bearer token: the user in bearer token did not match the user for session", + "token_user", fetchedToken.GetUser(), + "token", token, + "session_user", c.cfg.User, + "session_id", c.GetSessionID(), + ) return trace.AccessDenied("access denied") } @@ -260,7 +265,10 @@ func (c *SessionContext) remoteClient(ctx context.Context, site reversetunnelcli // the session context is closed. err = c.remoteClientCache.addRemoteClient(site, rClt) if err != nil { - c.cfg.Log.WithError(err).Info("Failed closing stale remote client for site: ", site.GetName()) + c.cfg.Log.InfoContext(ctx, "Failed closing stale remote client for site", + "remote_site", site.GetName(), + "error", err, + ) } return rClt, nil @@ -595,7 +603,7 @@ func (c *SessionContext) expired(ctx context.Context) bool { // was removed during user logout, expire the session immediately. return true default: - c.cfg.Log.WithError(err).Debug("Failed to query web session.") + c.cfg.Log.DebugContext(ctx, "Failed to query web session", "error", err) } // If the session has no expiry time, then also by definition it @@ -634,6 +642,7 @@ type sessionCacheOptions struct { sessionWatcherStartImmediately bool // See [sessionCache.sessionWatcherEventProcessedChannel]. Used for testing. sessionWatcherEventProcessedChannel chan struct{} + logger *slog.Logger } // newSessionCache creates a [sessionCache] from the provided [config] and @@ -649,6 +658,10 @@ func newSessionCache(ctx context.Context, config sessionCacheOptions) (*sessionC config.clock = clockwork.NewRealClock() } + if config.logger == nil { + config.logger = slog.Default() + } + cache := &sessionCache{ clusterName: clusterName.GetClusterName(), proxyClient: config.proxyClient, @@ -658,7 +671,7 @@ func newSessionCache(ctx context.Context, config sessionCacheOptions) (*sessionC authServers: config.servers, closer: utils.NewCloseBroadcaster(), cipherSuites: config.cipherSuites, - log: newPackageLogger(), + log: config.logger, clock: config.clock, sessionLingeringThreshold: config.sessionLingeringThreshold, proxySigner: config.proxySigner, @@ -678,7 +691,7 @@ func newSessionCache(ctx context.Context, config sessionCacheOptions) (*sessionC // sessionCache handles web session authentication, // and holds in-memory contexts associated with each session type sessionCache struct { - log logrus.FieldLogger + log *slog.Logger proxyClient authclient.ClientI authServers []utils.NetAddr accessPoint authclient.ReadProxyAccessPoint @@ -724,7 +737,7 @@ type sessionCache struct { // Close closes all allocated resources and stops goroutines func (s *sessionCache) Close() error { - s.log.Info("Closing session cache.") + s.log.InfoContext(context.Background(), "Closing session cache") return s.closer.Close() } @@ -757,8 +770,8 @@ func (s *sessionCache) clearExpiredSessions(ctx context.Context) { if !c.expired(ctx) { continue } - s.removeSessionContextLocked(c.cfg.Session.GetUser(), c.cfg.Session.GetName()) - s.log.WithField("ctx", c.String()).Debug("Context expired.") + s.removeSessionContextLocked(ctx, c.cfg.Session.GetUser(), c.cfg.Session.GetName()) + s.log.DebugContext(ctx, "Context expired", "context", logutils.StringerAttr(c)) } } @@ -775,12 +788,12 @@ func (s *sessionCache) watchWebSessions(ctx context.Context) { linear.First = 0 } - s.log.Debug("sessionCache: Starting WebSession watcher") + s.log.DebugContext(ctx, "sessionCache: Starting WebSession watcher") for { select { // Stop when the context tells us to. case <-ctx.Done(): - s.log.Debug("sessionCache: Stopping WebSession watcher") + s.log.DebugContext(ctx, "sessionCache: Stopping WebSession watcher") return case <-linear.After(): @@ -791,7 +804,7 @@ func (s *sessionCache) watchWebSessions(ctx context.Context) { const msg = "" + "sessionCache: WebSession watcher aborted, re-connecting. " + "This may have an impact in device trust web sessions." - s.log.WithError(err).Warn(msg) + s.log.WarnContext(ctx, msg, "error", err) } } } @@ -840,9 +853,9 @@ func (s *sessionCache) watchWebSessionsOnce(ctx context.Context, reset func()) e case event := <-watcher.Events(): reset() // Reset linear backoff attempts. - s.log. - WithField("event", event). - Trace("sessionCache: Received watcher event") + s.log.Log(ctx, logutils.TraceLevel, "sessionCache: Received watcher event", + "event", logutils.StringerAttr(event), + ) if event.Type != types.OpPut { continue // We only care about OpPut at the moment. @@ -850,25 +863,25 @@ func (s *sessionCache) watchWebSessionsOnce(ctx context.Context, reset func()) e session, ok := event.Resource.(types.WebSession) if !ok { - s.log. - WithField("resource_type", fmt.Sprintf("%T", event.Resource)). - Warn("sessionCache: Received unexpected resource type") + s.log.WarnContext(ctx, "sessionCache: Received unexpected resource type", + "resource_type", logutils.TypeAttr(event.Resource), + ) continue } if !session.GetHasDeviceExtensions() { - s.log. - WithField("session_id", session.GetName()). - Debug("sessionCache: Updated session doesn't have device extensions, skipping") + s.log.DebugContext(ctx, "sessionCache: Updated session doesn't have device extensions, skipping", + "session_id", session.GetName(), + ) notifyProcessed() continue } // Release existing and non-device-aware session. - if err := s.releaseResourcesIfNoDeviceExtensions(session.GetUser(), session.GetName()); err != nil { - s.log. - WithError(err). - WithField("session_id", session.GetName()). - Debug("sessionCache: Failed to release updated session") + if err := s.releaseResourcesIfNoDeviceExtensions(ctx, session.GetUser(), session.GetName()); err != nil { + s.log.DebugContext(ctx, "sessionCache: Failed to release updated session", + "error", err, + "session_id", session.GetName(), + ) } notifyProcessed() @@ -876,7 +889,7 @@ func (s *sessionCache) watchWebSessionsOnce(ctx context.Context, reset func()) e } } -func (s *sessionCache) releaseResourcesIfNoDeviceExtensions(user, sessionID string) error { +func (s *sessionCache) releaseResourcesIfNoDeviceExtensions(ctx context.Context, user, sessionID string) error { s.mu.Lock() defer s.mu.Unlock() @@ -885,16 +898,16 @@ func (s *sessionCache) releaseResourcesIfNoDeviceExtensions(user, sessionID stri case !ok: return nil // Session not found case sessionCtx.cfg.Session.GetHasDeviceExtensions(): - s.log. - WithField("session_id", sessionID). - Debug("sessionCache: Session already has device extensions, skipping") + s.log.DebugContext(ctx, "sessionCache: Session already has device extensions, skipping", + "session_id", sessionID, + ) return nil } - s.log. - WithField("session_id", sessionID). - Debug("sessionCache: Releasing session resources due to device extensions upgrade") - return s.releaseResourcesLocked(user, sessionID) + s.log.DebugContext(ctx, "sessionCache: Releasing session resources due to device extensions upgrade", + "session_id", sessionID, + ) + return s.releaseResourcesLocked(ctx, user, sessionID) } // AuthWithOTP authenticates the specified user with the given password and OTP token. @@ -1055,40 +1068,40 @@ func (s *sessionCache) insertContext(user string, sctx *SessionContext) (exists return false } -func (s *sessionCache) releaseResources(user, sessionID string) error { +func (s *sessionCache) releaseResources(ctx context.Context, user, sessionID string) error { s.mu.Lock() defer s.mu.Unlock() - return s.releaseResourcesLocked(user, sessionID) + return s.releaseResourcesLocked(ctx, user, sessionID) } -func (s *sessionCache) removeSessionContextLocked(user, sessionID string) error { +func (s *sessionCache) removeSessionContextLocked(ctx context.Context, user, sessionID string) error { id := sessionKey(user, sessionID) - ctx, ok := s.sessions[id] + sess, ok := s.sessions[id] if !ok { return nil } delete(s.sessions, id) - err := ctx.Close() + err := sess.Close() if err != nil { - s.log.WithFields(logrus.Fields{ - "ctx": ctx.String(), - logrus.ErrorKey: err, - }).Warn("Failed to close session context.") + s.log.WarnContext(ctx, "Failed to close session context", + "context", logutils.StringerAttr(sess), + "error", err, + ) return trace.Wrap(err) } return nil } -func (s *sessionCache) releaseResourcesLocked(user, sessionID string) error { +func (s *sessionCache) releaseResourcesLocked(ctx context.Context, user, sessionID string) error { var errors []error - err := s.removeSessionContextLocked(user, sessionID) + err := s.removeSessionContextLocked(ctx, user, sessionID) if err != nil { errors = append(errors, err) } - if ctx, ok := s.resources[user]; ok { + if sess, ok := s.resources[user]; ok { delete(s.resources, user) - if err := ctx.Close(); err != nil { - s.log.WithError(err).Warn("Failed to clean up session context.") + if err := sess.Close(); err != nil { + s.log.WarnContext(ctx, "Failed to clean up session context", "error", err) errors = append(errors, err) } } @@ -1102,10 +1115,10 @@ func (s *sessionCache) upsertSessionContext(user string) *sessionResources { return ctx } ctx := &sessionResources{ - log: s.log.WithFields(logrus.Fields{ - teleport.ComponentKey: "user-session", - "user": user, - }), + log: s.log.With( + teleport.ComponentKey, "user-session", + "user", user, + ), } s.resources[user] = ctx return ctx @@ -1143,10 +1156,10 @@ func (s *sessionCache) newSessionContextFromSession(ctx context.Context, session } sctx, err := NewSessionContext(SessionContextConfig{ - Log: s.log.WithFields(logrus.Fields{ - "user": session.GetUser(), - "session": session.GetShortName(), - }), + Log: s.log.With( + "user", session.GetUser(), + "session", session.GetShortName(), + ), User: session.GetUser(), RootClient: userClient, UnsafeCachedAuthClient: s.accessPoint, @@ -1217,7 +1230,6 @@ func (c *sessionResources) Close() error { closers := c.transferClosers() var errors []error for _, closer := range closers { - c.log.Debugf("Closing %v.", closer) if err := closer.Close(); err != nil { errors = append(errors, err) } @@ -1228,7 +1240,7 @@ func (c *sessionResources) Close() error { // sessionResources persists resources initiated by a web session // but which might outlive the session. type sessionResources struct { - log logrus.FieldLogger + log *slog.Logger mu sync.Mutex closers []io.Closer @@ -1336,7 +1348,7 @@ const ( // the server to send the session ID it's using. The returned function // will return the current session ID from the server or a reason why // one wasn't received. -func prepareToReceiveSessionID(ctx context.Context, log *logrus.Entry, nc *client.NodeClient) func() (session.ID, sessionIDStatus) { +func prepareToReceiveSessionID(ctx context.Context, log *slog.Logger, nc *client.NodeClient) func() (session.ID, sessionIDStatus) { // send the session ID received from the server var gotSessionID atomic.Bool sessionIDFromServer := make(chan session.ID, 1) @@ -1349,7 +1361,7 @@ func prepareToReceiveSessionID(ctx context.Context, log *logrus.Entry, nc *clien sid, err := session.ParseID(string(req.Payload)) if err != nil { - log.WithError(err).Warn("Unable to parse session ID.") + log.WarnContext(ctx, "Unable to parse session ID", "error", err) return nil } @@ -1368,7 +1380,7 @@ func prepareToReceiveSessionID(ctx context.Context, log *logrus.Entry, nc *clien go func() { resp, _, err := nc.Client.SendRequest(ctx, teleport.SessionIDQueryRequest, true, nil) if err != nil { - log.WithError(err).Warn("Failed to send session ID query request") + log.WarnContext(ctx, "Failed to send session ID query request", "error", err) serverWillSetSessionID <- false } else { serverWillSetSessionID <- resp diff --git a/lib/web/terminal.go b/lib/web/terminal.go index 9326140447eac..d9f20cd5396d7 100644 --- a/lib/web/terminal.go +++ b/lib/web/terminal.go @@ -39,7 +39,6 @@ import ( "github.com/gorilla/websocket" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" oteltrace "go.opentelemetry.io/otel/trace" "golang.org/x/crypto/ssh" @@ -123,10 +122,6 @@ func NewTerminal(ctx context.Context, cfg TerminalHandlerConfig) (*TerminalHandl return &TerminalHandler{ sshBaseHandler: sshBaseHandler{ - log: logrus.WithFields(logrus.Fields{ - teleport.ComponentKey: teleport.ComponentWebsocket, - "session_id": cfg.SessionData.ID.String(), - }), logger: cfg.Logger.With( teleport.ComponentKey, teleport.ComponentWebsocket, "session_id", cfg.SessionData.ID.String(), @@ -268,8 +263,6 @@ func (t *TerminalHandlerConfig) CheckAndSetDefaults() error { // sshBaseHandler is a base handler for web SSH connections. type sshBaseHandler struct { - // log holds the structured logger. - log *logrus.Entry // logger holds the structured logger. logger *slog.Logger // ctx is a web session context for the currently logged-in user. @@ -365,14 +358,14 @@ func (t *TerminalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { err := ws.SetReadDeadline(deadlineForInterval(t.keepAliveInterval)) if err != nil { - t.log.WithError(err).Error("Error setting websocket readline") + t.logger.ErrorContext(r.Context(), "Error setting websocket readline", "error", err) return } t.handler(ws, r) } -func (t *TerminalHandler) writeSessionData() error { +func (t *TerminalHandler) writeSessionData(ctx context.Context) error { envelope := &terminal.Envelope{ Version: defaults.WebsocketVersion, Type: defaults.WebsocketSessionMetadata, @@ -387,7 +380,7 @@ func (t *TerminalHandler) writeSessionData() error { sessionDataTemp.Login = t.displayLogin sessionMetadataResponse, err := json.Marshal(siteSessionGenerateResponse{Session: sessionDataTemp}) if err != nil { - t.sendError("unable to marshal session response", err, t.stream) + t.sendError(ctx, "unable to marshal session response", err, t.stream) return trace.Wrap(err) } envelope.Payload = string(sessionMetadataResponse) @@ -406,7 +399,7 @@ func (t *TerminalHandler) writeSessionData() error { sessionMetadataResponse, err := json.Marshal(siteSessionGenerateResponse{Session: sessionDataTemp}) if err != nil { - t.sendError("unable to marshal session response", err, t.stream) + t.sendError(ctx, "unable to marshal session response", err, t.stream) return trace.Wrap(err) } envelope.Payload = string(sessionMetadataResponse) @@ -414,12 +407,12 @@ func (t *TerminalHandler) writeSessionData() error { envelopeBytes, err := proto.Marshal(envelope) if err != nil { - t.sendError("unable to marshal session data event for web client", err, t.stream) + t.sendError(ctx, "unable to marshal session data event for web client", err, t.stream) return trace.Wrap(err) } if err := t.stream.WriteMessage(websocket.BinaryMessage, envelopeBytes); err != nil { - t.sendError("unable to write message to socket", err, t.stream) + t.sendError(ctx, "unable to write message to socket", err, t.stream) return trace.Wrap(err) } @@ -455,23 +448,23 @@ func (t *TerminalHandler) handler(ws *websocket.Conn, r *http.Request) { tctx := oteltrace.ContextWithRemoteSpanContext(context.Background(), oteltrace.SpanContextFromContext(r.Context())) ctx, cancel := context.WithCancel(tctx) defer cancel() - t.stream = terminal.NewStream(ctx, terminal.StreamConfig{WS: ws, Logger: t.log}) + t.stream = terminal.NewStream(ctx, terminal.StreamConfig{WS: ws, Logger: t.logger}) // Create a Teleport client, if not able to, show the reason to the user in // the terminal. tc, err := t.makeClient(ctx, t.stream, ws.RemoteAddr().String()) if err != nil { - t.log.WithError(err).Info("Failed creating a client for session") - t.stream.WriteError(err.Error()) + t.logger.InfoContext(ctx, "Failed creating a client for session", "error", err) + t.stream.WriteError(ctx, err.Error()) return } - t.log.Debug("Creating websocket stream") + t.logger.DebugContext(ctx, "Creating websocket stream") defaultCloseHandler := ws.CloseHandler() ws.SetCloseHandler(func(code int, text string) error { t.closedByClient.Store(true) - t.log.Debug("web socket was closed by client - terminating session") + t.logger.DebugContext(ctx, "web socket was closed by client - terminating session") // Call the default close handler if one was set. if defaultCloseHandler != nil { @@ -490,7 +483,7 @@ func (t *TerminalHandler) handler(ws *websocket.Conn, r *http.Request) { // Block until the terminal session is complete. t.streamTerminal(ctx, tc) - t.log.Debug("Closing websocket stream") + t.logger.DebugContext(ctx, "Closing websocket stream") } type stderrWriter struct { @@ -498,7 +491,7 @@ type stderrWriter struct { } func (s stderrWriter) Write(b []byte) (int, error) { - s.stream.WriteError(string(b)) + s.stream.WriteError(context.Background(), string(b)) return len(b), nil } @@ -547,12 +540,12 @@ func (t *TerminalHandler) makeClient(ctx context.Context, stream *terminal.Strea // The web session was closed by the client while the ssh connection was being established. // Attempt to close the SSH session instead of proceeding with the window change request. if t.closedByClient.Load() { - t.log.Debug("websocket was closed by client, terminating established ssh connection to host") + t.logger.DebugContext(ctx, "websocket was closed by client, terminating established ssh connection to host") return false, trace.Wrap(s.Close()) } if err := s.WindowChange(ctx, t.term.H, t.term.W); err != nil { - t.log.Error(err) + t.logger.ErrorContext(ctx, "failed to send window change request", "error", err) } return false, nil @@ -569,7 +562,7 @@ func (t *sshBaseHandler) issueSessionMFACerts(ctx context.Context, tc *client.Te ctx, span := t.tracer.Start(ctx, "terminal/issueSessionMFACerts") defer span.End() - log.Debug("Attempting to issue a single-use user certificate with an MFA check.") + t.logger.DebugContext(ctx, "Attempting to issue a single-use user certificate with an MFA check") // Prepare MFA check request. mfaRequiredReq := &authproto.IsMFARequiredRequest{ @@ -812,8 +805,8 @@ func (t *TerminalHandler) streamTerminal(ctx context.Context, tc *client.Telepor nc, err := t.connectToHost(ctx, t.stream, tc, t.connectToNodeWithMFA) if err != nil { - t.log.WithError(err).Warn("Unable to stream terminal - failure connecting to host") - t.stream.WriteError(err.Error()) + t.logger.WarnContext(ctx, "Unable to stream terminal - failure connecting to host", "error", err) + t.stream.WriteError(ctx, err.Error()) return } defer nc.Close() @@ -823,7 +816,7 @@ func (t *TerminalHandler) streamTerminal(ctx context.Context, tc *client.Telepor // by the client from here on out should either get caught in the OnShellCreated callback // set on the [tc] or in [TerminalHandler.Close]. if t.closedByClient.Load() { - t.log.Debug("websocket was closed by client, aborting establishing ssh connection to host") + t.logger.DebugContext(ctx, "websocket was closed by client, aborting establishing ssh connection to host") return } @@ -833,7 +826,7 @@ func (t *TerminalHandler) streamTerminal(ctx context.Context, tc *client.Telepor nc.OnMFA = func() { baseCeremony := newMFACeremony(t.stream.WSStream, nil) if err := t.presenceChecker(ctx, out, t.userAuthClient, t.sessionData.ID.String(), baseCeremony); err != nil { - t.log.WithError(err).Warn("Unable to stream terminal - failure performing presence checks") + t.logger.WarnContext(ctx, "Unable to stream terminal - failure performing presence checks", "error", err) return } } @@ -844,7 +837,7 @@ func (t *TerminalHandler) streamTerminal(ctx context.Context, tc *client.Telepor defer monitorCancel() go func() { if err := monitorSessionLatency(monitorCtx, t.clock, t.stream.WSStream, nc.Client); err != nil { - t.log.WithError(err).Warn("failure monitoring session latency") + t.logger.WarnContext(monitorCtx, "failure monitoring session latency", "error", err) } }() @@ -852,8 +845,8 @@ func (t *TerminalHandler) streamTerminal(ctx context.Context, tc *client.Telepor // If we are joining a session, send the session data right away, we // know the session ID if t.tracker != nil { - if err := t.writeSessionData(); err != nil { - t.log.WithError(err).Warn("Failure sending session data") + if err := t.writeSessionData(ctx); err != nil { + t.logger.WarnContext(ctx, "Failure sending session data", "error", err) } close(sessionDataSent) } else { @@ -862,7 +855,7 @@ func (t *TerminalHandler) streamTerminal(ctx context.Context, tc *client.Telepor // created and the server sends us the session ID it is using writeSessionCtx, writeSessionCancel := context.WithCancel(ctx) defer writeSessionCancel() - waitForSessionID := prepareToReceiveSessionID(writeSessionCtx, t.log, nc) + waitForSessionID := prepareToReceiveSessionID(writeSessionCtx, t.logger, nc) // wait in a new goroutine because the server won't set a // session ID until we open a shell @@ -875,13 +868,13 @@ func (t *TerminalHandler) streamTerminal(ctx context.Context, tc *client.Telepor t.sessionData.ID = sid fallthrough case sessionIDNotModified: - if err := t.writeSessionData(); err != nil { - t.log.WithError(err).Warn("Failure sending session data") + if err := t.writeSessionData(ctx); err != nil { + t.logger.WarnContext(ctx, "Failure sending session data", "error", err) } case sessionIDNotSent: - t.log.Warn("Failed to receive session data") + t.logger.WarnContext(ctx, "Failed to receive session data") default: - t.log.Warnf("Invalid session ID status %v", status) + t.logger.WarnContext(ctx, "Invalid session ID status", "status", status) } }() } @@ -890,7 +883,7 @@ func (t *TerminalHandler) streamTerminal(ctx context.Context, tc *client.Telepor // either an error occurs or it completes successfully. if err = nc.RunInteractiveShell(ctx, t.participantMode, t.tracker, nil, beforeStart); err != nil { if !t.closedByClient.Load() { - t.stream.WriteError(err.Error()) + t.stream.WriteError(ctx, err.Error()) } return } @@ -904,15 +897,15 @@ func (t *TerminalHandler) streamTerminal(ctx context.Context, tc *client.Telepor // Send close envelope to web terminal upon exit without an error. if err := t.stream.SendCloseMessage(t.sessionData.ServerID); err != nil { - t.log.WithError(err).Error("Unable to send close event to web client.") + t.logger.ErrorContext(ctx, "Unable to send close event to web client", "error", err) } if err := t.stream.Close(); err != nil && !errors.Is(err, io.EOF) { - t.log.WithError(err).Error("Unable to close client web socket.") + t.logger.ErrorContext(ctx, "Unable to close client web socket", "error", err) return } - t.log.Debug("Sent close event to web client.") + t.logger.DebugContext(ctx, "Sent close event to web client") } // connectToNode attempts to connect to the host with the already @@ -920,7 +913,7 @@ func (t *TerminalHandler) streamTerminal(ctx context.Context, tc *client.Telepor func (t *sshBaseHandler) connectToNode(ctx context.Context, ws terminal.WSConn, tc *client.TeleportClient, accessChecker services.AccessChecker, getAgent teleagent.Getter, signer agentless.SignerCreator) (*client.NodeClient, error) { conn, err := t.router.DialHost(ctx, ws.RemoteAddr(), ws.LocalAddr(), t.sessionData.ServerID, strconv.Itoa(t.sessionData.ServerHostPort), tc.SiteName, accessChecker, getAgent, signer) if err != nil { - t.log.WithError(err).Warn("Unable to stream terminal - failed to dial host.") + t.logger.WarnContext(ctx, "Unable to stream terminal - failed to dial host", "error", err) if errors.Is(err, trace.NotFound(teleport.NodeIsAmbiguous)) { const message = "error: ambiguous host could match multiple nodes\n\nHint: try addressing the node by unique id (ex: user@node-id)\n" @@ -1010,7 +1003,7 @@ func (t *sshBaseHandler) connectToNodeWithMFABase(ctx context.Context, ws termin } // sendError sends an error message to the client using the provided websocket. -func (t *sshBaseHandler) sendError(errMsg string, err error, ws terminal.WSConn) { +func (t *sshBaseHandler) sendError(ctx context.Context, errMsg string, err error, ws terminal.WSConn) { envelope := &terminal.Envelope{ Version: defaults.WebsocketVersion, Type: defaults.WebsocketError, @@ -1019,10 +1012,10 @@ func (t *sshBaseHandler) sendError(errMsg string, err error, ws terminal.WSConn) envelopeBytes, err := proto.Marshal(envelope) if err != nil { - t.log.WithError(err).Error("failed to marshal error message") + t.logger.ErrorContext(ctx, "failed to marshal error message", "error", err) } if err := ws.WriteMessage(websocket.BinaryMessage, envelopeBytes); err != nil { - t.log.WithError(err).Error("failed to send error message") + t.logger.ErrorContext(ctx, "failed to send error message", "error", err) } } @@ -1033,22 +1026,22 @@ func (t *TerminalHandler) streamEvents(ctx context.Context, tc *client.TeleportC select { // Send push events that come over the events channel to the web client. case event := <-tc.EventsChannel(): - logger := t.log.WithField("event", event.GetType()) + logger := t.logger.With("event", event.GetType()) data, err := json.Marshal(event) if err != nil { - logger.WithError(err).Error("Unable to marshal audit event") + logger.ErrorContext(ctx, "Unable to marshal audit event", "error", err) continue } - logger.Debug("Sending audit event to web client.") + logger.DebugContext(ctx, "Sending audit event to web client") if err := t.stream.WriteAuditEvent(data); err != nil { if errors.Is(err, websocket.ErrCloseSent) { - logger.WithError(err).Debug("Websocket was closed, no longer streaming events") + logger.DebugContext(ctx, "Websocket was closed, no longer streaming events", "error", err) return } - logger.WithError(err).Error("Unable to send audit event to web client") + logger.ErrorContext(ctx, "Unable to send audit event to web client", "error", err) continue } diff --git a/lib/web/terminal/terminal.go b/lib/web/terminal/terminal.go index 334b7adb1b440..72c9b2d7499c1 100644 --- a/lib/web/terminal/terminal.go +++ b/lib/web/terminal/terminal.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net" "strings" "sync" @@ -30,7 +31,6 @@ import ( "github.com/gogo/protobuf/proto" "github.com/gorilla/websocket" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "golang.org/x/text/encoding" "golang.org/x/text/encoding/unicode" @@ -41,6 +41,7 @@ import ( "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/session" "github.com/gravitational/teleport/lib/utils" + logutils "github.com/gravitational/teleport/lib/utils/log" ) // WSConn is a gorilla/websocket minimal interface used by our web implementation. @@ -95,7 +96,7 @@ type WSStream struct { WSConn // log holds the structured logger. - log logrus.FieldLogger + log *slog.Logger } // Replace \n with \r\n so the message is correctly aligned. @@ -114,9 +115,9 @@ func (t *WSStream) WriteMessage(messageType int, data []byte) error { } // WriteError displays an error in the terminal window. -func (t *WSStream) WriteError(msg string) { +func (t *WSStream) WriteError(ctx context.Context, msg string) { if _, err := replacer.WriteString(t, msg); err != nil && !errors.Is(err, websocket.ErrCloseSent) { - t.log.WithError(err).Warnf("Unable to send error to terminal: %v", msg) + t.log.WarnContext(ctx, "Unable to send error to terminal", "message", msg, "error", err) } } @@ -156,19 +157,19 @@ func (t *WSStream) processMessages(ctx context.Context) { select { case <-ctx.Done(): default: - t.WriteError(msg) + t.WriteError(ctx, msg) return } } if ty != websocket.BinaryMessage { - t.WriteError(fmt.Sprintf("Expected binary message, got %v", ty)) + t.WriteError(ctx, fmt.Sprintf("Expected binary message, got %v", ty)) return } var envelope Envelope if err := proto.Unmarshal(bytes, &envelope); err != nil { - t.WriteError(fmt.Sprintf("Unable to parse message payload %v", err)) + t.WriteError(ctx, fmt.Sprintf("Unable to parse message payload %v", err)) return } @@ -196,7 +197,7 @@ func (t *WSStream) processMessages(ctx context.Context) { handler, ok := t.handlers[envelope.Type] if !ok { - t.log.Warnf("Received web socket envelope with unknown type %v", envelope.Type) + t.log.WarnContext(ctx, "Received web socket envelope with unknown type", "envelope_type", logutils.TypeAttr(envelope.Type)) continue } @@ -427,13 +428,13 @@ type StreamConfig struct { // The websocket to operate over. Required. WS WSConn // A logger to emit log messages. Optional. - Logger logrus.FieldLogger + Logger *slog.Logger // A custom set of handlers to process messages received // over the websocket. Optional. Handlers map[string]WSHandlerFunc } -func NewWStream(ctx context.Context, ws WSConn, log logrus.FieldLogger, handlers map[string]WSHandlerFunc) *WSStream { +func NewWStream(ctx context.Context, ws WSConn, log *slog.Logger, handlers map[string]WSHandlerFunc) *WSStream { w := &WSStream{ log: log, WSConn: ws, @@ -473,7 +474,7 @@ func NewStream(ctx context.Context, cfg StreamConfig) *Stream { } if cfg.Logger == nil { - cfg.Logger = utils.NewLogger() + cfg.Logger = slog.Default() } t.WSStream = NewWStream(ctx, cfg.WS, cfg.Logger, cfg.Handlers) @@ -497,19 +498,19 @@ func (t *Stream) handleWindowResize(ctx context.Context, envelope Envelope) { var e map[string]interface{} err := json.Unmarshal([]byte(envelope.Payload), &e) if err != nil { - t.log.Warnf("Failed to parse resize payload: %v", err) + t.log.WarnContext(ctx, "Failed to parse resize payload", "error", err) return } size, ok := e["size"].(string) if !ok { - t.log.Errorf("expected size to be of type string, got type %T instead", size) + t.log.ErrorContext(ctx, "got unexpected size type, expected type string", "size_type", logutils.TypeAttr(size)) return } params, err := session.UnmarshalTerminalParams(size) if err != nil { - t.log.Warnf("Failed to retrieve terminal size: %v", err) + t.log.WarnContext(ctx, "Failed to retrieve terminal size", "error", err) return } @@ -519,7 +520,7 @@ func (t *Stream) handleWindowResize(ctx context.Context, envelope Envelope) { } if err := t.sshSession.WindowChange(ctx, params.H, params.W); err != nil { - t.log.Error(err) + t.log.ErrorContext(ctx, "failed to send window change request", "error", err) } } @@ -541,7 +542,7 @@ func (t *Stream) handleFileTransferDecision(ctx context.Context, envelope Envelo } approved, ok := e["approved"].(bool) if !ok { - t.log.Error("Unable to find approved status on response") + t.log.ErrorContext(ctx, "Unable to find approved status on response") return } @@ -551,7 +552,7 @@ func (t *Stream) handleFileTransferDecision(ctx context.Context, envelope Envelo err = t.sshSession.DenyFileTransferRequest(ctx, e.GetString("requestId")) } if err != nil { - t.log.WithError(err).Error("Unable to respond to file transfer request") + t.log.ErrorContext(ctx, "Unable to respond to file transfer request", "error", err) } } @@ -573,7 +574,7 @@ func (t *Stream) handleFileTransferRequest(ctx context.Context, envelope Envelop } download, ok := e["download"].(bool) if !ok { - t.log.Error("Unable to find download param in response") + t.log.ErrorContext(ctx, "Unable to find download param in response") return } @@ -582,7 +583,7 @@ func (t *Stream) handleFileTransferRequest(ctx context.Context, envelope Envelop Location: e.GetString("location"), Filename: e.GetString("filename"), }); err != nil { - t.log.WithError(err).Error("Unable to request file transfer") + t.log.ErrorContext(ctx, "Unable to request file transfer", "error", err) } } diff --git a/lib/web/terminal_test.go b/lib/web/terminal_test.go index 615993c89710a..ea6182464bf9b 100644 --- a/lib/web/terminal_test.go +++ b/lib/web/terminal_test.go @@ -24,6 +24,7 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" "net/http/httptest" "net/url" @@ -77,7 +78,7 @@ func TestTerminalReadFromClosedConn(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - stream := terminal.NewStream(ctx, terminal.StreamConfig{WS: conn, Logger: utils.NewLoggerForTests()}) + stream := terminal.NewStream(ctx, terminal.StreamConfig{WS: conn, Logger: utils.NewSlogLoggerForTests()}) // close the stream before we attempt to read from it, // this will produce a net.ErrClosed error on the read @@ -203,7 +204,7 @@ func connectToHost(ctx context.Context, cfg connectConfig) (*testTerminal, error t.stream = terminal.NewStream(ctx, terminal.StreamConfig{ WS: ws, - Logger: utils.NewLogger(), + Logger: slog.Default(), Handlers: cfg.handlers, }) diff --git a/lib/web/tty_playback.go b/lib/web/tty_playback.go index 76c456a4a7010..838c1b45d1efd 100644 --- a/lib/web/tty_playback.go +++ b/lib/web/tty_playback.go @@ -34,6 +34,7 @@ import ( "github.com/gravitational/teleport/lib/reversetunnelclient" "github.com/gravitational/teleport/lib/session" "github.com/gravitational/teleport/lib/utils" + logutils "github.com/gravitational/teleport/lib/utils/log" ) const ( @@ -111,14 +112,14 @@ func (h *Handler) ttyPlaybackHandle( return nil, trace.Wrap(err) } - h.log.Debug("upgrading to websocket") + h.logger.DebugContext(r.Context(), "upgrading to websocket") upgrader := websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } ws, err := upgrader.Upgrade(w, r, nil) if err != nil { - h.log.Warn("failed upgrade", err) + h.logger.WarnContext(r.Context(), "failed upgrade", "error", err) // if Upgrade fails, it automatically replies with an HTTP error // (this means we don't need to return an error here) return nil, nil @@ -132,7 +133,7 @@ func (h *Handler) ttyPlaybackHandle( Context: r.Context(), }) if err != nil { - h.log.Warn("player error", err) + h.logger.WarnContext(r.Context(), "player error", "error", err) writeError(ws, err) return nil, nil } @@ -146,18 +147,18 @@ func (h *Handler) ttyPlaybackHandle( typ, b, err := ws.ReadMessage() if err != nil { if !utils.IsOKNetworkError(err) { - log.Warnf("websocket read error: %v", err) + h.logger.WarnContext(ctx, "websocket read error", "error", err) } return } if typ != websocket.BinaryMessage { - log.Debugf("skipping unknown websocket message type %v", typ) + h.logger.DebugContext(ctx, "skipping unknown websocket message type", "message_type", logutils.TypeAttr(typ)) continue } if err := handlePlaybackAction(b, player); err != nil { - log.Warnf("skipping bad action: %v", err) + h.logger.WarnContext(ctx, "skipping bad action", "error", err) continue } } @@ -166,12 +167,12 @@ func (h *Handler) ttyPlaybackHandle( go func() { defer cancel() defer func() { - h.log.Debug("closing websocket") + h.logger.DebugContext(ctx, "closing websocket") if err := ws.WriteMessage(websocket.CloseMessage, nil); err != nil { - h.log.Debugf("error sending close message: %v", err) + h.logger.DebugContext(r.Context(), "error sending close message", "error", err) } if err := ws.Close(); err != nil { - h.log.Debugf("error closing websocket: %v", err) + h.logger.DebugContext(ctx, "error closing websocket", "error", err) } }() @@ -209,7 +210,7 @@ func (h *Handler) ttyPlaybackHandle( writeSize := func(size string) error { ts, err := session.UnmarshalTerminalParams(size) if err != nil { - h.log.Debugf("Ignoring invalid terminal size %q", size) + h.logger.DebugContext(ctx, "Ignoring invalid terminal size", "terminal_size", size) return nil // don't abort playback due to a bad event } @@ -230,7 +231,7 @@ func (h *Handler) ttyPlaybackHandle( if !ok { // send any playback errors to the browser if err := writeError(ws, player.Err()); err != nil { - h.log.Warnf("failed to send error message to browser: %v", err) + h.logger.WarnContext(ctx, "failed to send error message to browser", "error", err) } return } @@ -238,13 +239,13 @@ func (h *Handler) ttyPlaybackHandle( switch evt := evt.(type) { case *events.SessionStart: if err := writeSize(evt.TerminalSize); err != nil { - h.log.Debugf("Failed to write resize: %v", err) + h.logger.DebugContext(ctx, "Failed to write resize", "error", err) return } case *events.SessionPrint: if err := writePTY(evt.Data, uint64(evt.DelayMilliseconds)); err != nil { - h.log.Debugf("Failed to send PTY data: %v", err) + h.logger.DebugContext(ctx, "Failed to send PTY data", "error", err) return } @@ -253,20 +254,20 @@ func (h *Handler) ttyPlaybackHandle( // at the end of the recording is processed and the allow // the progress bar to go to 100% if err := writePTY(nil, uint64(evt.EndTime.Sub(evt.StartTime)/time.Millisecond)); err != nil { - h.log.Debugf("Failed to send session end data: %v", err) + h.logger.DebugContext(ctx, "Failed to send session end data", "error", err) return } case *events.Resize: if err := writeSize(evt.TerminalSize); err != nil { - h.log.Debugf("Failed to write resize: %v", err) + h.logger.DebugContext(ctx, "Failed to write resize", "error", err) return } case *events.SessionLeave: // do nothing default: - h.log.Debugf("unexpected event type %T", evt) + h.logger.DebugContext(ctx, "unexpected event type", "event_type", logutils.TypeAttr(evt)) } } } diff --git a/lib/web/ui/app.go b/lib/web/ui/app.go index 5661d3290960c..934978ccb8d44 100644 --- a/lib/web/ui/app.go +++ b/lib/web/ui/app.go @@ -20,10 +20,10 @@ package ui import ( "cmp" + "context" + "log/slog" "sort" - "github.com/sirupsen/logrus" - "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/ui" "github.com/gravitational/teleport/lib/utils" @@ -113,7 +113,7 @@ type MakeAppsConfig struct { // UserGroupLookup is a map of user groups to provide to each App UserGroupLookup map[string]types.UserGroup // Logger is a logger used for debugging while making an app - Logger logrus.FieldLogger + Logger *slog.Logger // RequireRequest indicates if a returned resource is only accessible after an access request RequiresRequest bool } @@ -126,7 +126,7 @@ func MakeApp(app types.Application, c MakeAppsConfig) App { for _, userGroupName := range app.GetUserGroups() { userGroup := c.UserGroupLookup[userGroupName] if userGroup == nil { - c.Logger.Debugf("Unable to find user group %s when creating user groups, skipping", userGroupName) + c.Logger.DebugContext(context.Background(), "Unable to find user group when creating user groups, skipping", "user_group", userGroupName) continue } diff --git a/lib/web/user_groups.go b/lib/web/user_groups.go index 4e0927dabf55e..a61340491e8d8 100644 --- a/lib/web/user_groups.go +++ b/lib/web/user_groups.go @@ -57,7 +57,7 @@ func (h *Handler) getUserGroups(_ http.ResponseWriter, r *http.Request, params h UseSearchAsRoles: true, }) if err != nil { - h.log.Debugf("Unable to fetch applications while listing user groups, unable to display associated applications: %v", err) + h.logger.DebugContext(r.Context(), "Unable to fetch applications while listing user groups, unable to display associated applications", "error", err) } appServerLookup := make(map[string]types.AppServer, len(appServers)) @@ -71,7 +71,7 @@ func (h *Handler) getUserGroups(_ http.ResponseWriter, r *http.Request, params h for _, appName := range userGroup.GetApplications() { app := appServerLookup[appName] if app == nil { - h.log.Debugf("Unable to find application %s when creating user groups, skipping", appName) + h.logger.DebugContext(r.Context(), "Unable to find application when creating user groups, skipping", "app", appName) continue } apps = append(apps, app.GetApp()) diff --git a/lib/web/web.go b/lib/web/web.go deleted file mode 100644 index ab73f43324f92..0000000000000 --- a/lib/web/web.go +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package web - -import ( - "github.com/sirupsen/logrus" - - "github.com/gravitational/teleport" -) - -var log = newPackageLogger() - -// newPackageLogger returns a new instance of the logger -// configured for the package -func newPackageLogger(subcomponents ...string) logrus.FieldLogger { - return logrus.WithField(teleport.ComponentKey, - teleport.Component(append([]string{teleport.ComponentWeb}, subcomponents...)...)) -} From 47c021650cf76fdc69fd5ed49f819a176b2f9fee Mon Sep 17 00:00:00 2001 From: rosstimothy <39066650+rosstimothy@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:41:37 -0500 Subject: [PATCH 28/64] Convert lib/devicetrust to use slog (#50458) --- lib/auth/auth_with_roles.go | 8 ++-- lib/authz/permissions.go | 2 +- lib/devicetrust/authz/authz.go | 18 +++---- lib/devicetrust/authz/authz_test.go | 5 +- lib/devicetrust/enroll/enroll.go | 25 ++++++---- lib/devicetrust/errors.go | 7 +-- lib/devicetrust/native/api.go | 5 +- lib/devicetrust/native/device_darwin.go | 7 +-- lib/devicetrust/native/device_linux.go | 34 +++++++------ lib/devicetrust/native/device_linux_test.go | 4 -- lib/devicetrust/native/device_windows.go | 19 ++++---- lib/devicetrust/native/tpm_device.go | 53 ++++++++++++++------- lib/srv/session_control.go | 2 +- 13 files changed, 110 insertions(+), 79 deletions(-) diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index e5fdaa9dab8ce..4e463f6d7b8b1 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -3021,7 +3021,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC if err != nil { return nil, trace.Wrap(err) } - if err := a.verifyUserDeviceForCertIssuance(req.Usage, readOnlyAuthPref.GetDeviceTrust()); err != nil { + if err := a.verifyUserDeviceForCertIssuance(ctx, req.Usage, readOnlyAuthPref.GetDeviceTrust()); err != nil { return nil, trace.Wrap(err) } @@ -3417,14 +3417,14 @@ func (a *ServerWithRoles) GetAccessRequestAllowedPromotions(ctx context.Context, // is not paramount to the access system itself, but it stops bad attempts from // progressing further and provides better feedback than other protocol-specific // failures. -func (a *ServerWithRoles) verifyUserDeviceForCertIssuance(usage proto.UserCertsRequest_CertUsage, dt *types.DeviceTrust) error { +func (a *ServerWithRoles) verifyUserDeviceForCertIssuance(ctx context.Context, usage proto.UserCertsRequest_CertUsage, dt *types.DeviceTrust) error { // Ignore App or WindowsDeskop requests, they do not support device trust. if usage == proto.UserCertsRequest_App || usage == proto.UserCertsRequest_WindowsDesktop { return nil } identity := a.context.Identity.GetIdentity() - return trace.Wrap(dtauthz.VerifyTLSUser(dt, identity)) + return trace.Wrap(dtauthz.VerifyTLSUser(ctx, dt, identity)) } func (a *ServerWithRoles) CreateResetPasswordToken(ctx context.Context, req authclient.CreateUserTokenRequest) (types.UserToken, error) { @@ -6813,7 +6813,7 @@ func (a *ServerWithRoles) enforceGlobalModeTrustedDevice(ctx context.Context) er return trace.Wrap(err) } - err = dtauthz.VerifyTLSUser(readOnlyAuthPref.GetDeviceTrust(), a.context.Identity.GetIdentity()) + err = dtauthz.VerifyTLSUser(ctx, readOnlyAuthPref.GetDeviceTrust(), a.context.Identity.GetIdentity()) return trace.Wrap(err) } diff --git a/lib/authz/permissions.go b/lib/authz/permissions.go index 66fb0c6a86eeb..e905d6d9d479f 100644 --- a/lib/authz/permissions.go +++ b/lib/authz/permissions.go @@ -443,7 +443,7 @@ func (a *authorizer) Authorize(ctx context.Context) (authCtx *Context, err error // Device Trust: authorize device extensions. if !a.disableGlobalDeviceMode { - if err := dtauthz.VerifyTLSUser(authPref.GetDeviceTrust(), authContext.Identity.GetIdentity()); err != nil { + if err := dtauthz.VerifyTLSUser(ctx, authPref.GetDeviceTrust(), authContext.Identity.GetIdentity()); err != nil { return nil, trace.Wrap(err) } } diff --git a/lib/devicetrust/authz/authz.go b/lib/devicetrust/authz/authz.go index 3bcc0ad3bf812..5f95b8af8eace 100644 --- a/lib/devicetrust/authz/authz.go +++ b/lib/devicetrust/authz/authz.go @@ -19,8 +19,10 @@ package authz import ( + "context" + "log/slog" + "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" @@ -45,8 +47,8 @@ func IsTLSDeviceVerified(ext *tlsca.DeviceExtensions) bool { // VerifyTLSUser verifies if the TLS identity has the required extensions to // fulfill the device trust configuration. -func VerifyTLSUser(dt *types.DeviceTrust, identity tlsca.Identity) error { - return verifyDeviceExtensions(dt, identity.Username, IsTLSDeviceVerified(&identity.DeviceExtensions)) +func VerifyTLSUser(ctx context.Context, dt *types.DeviceTrust, identity tlsca.Identity) error { + return verifyDeviceExtensions(ctx, dt, identity.Username, IsTLSDeviceVerified(&identity.DeviceExtensions)) } // IsSSHDeviceVerified returns true if cert contains all required device @@ -83,24 +85,22 @@ func HasDeviceTrustExtensions(extensions []string) bool { // VerifySSHUser verifies if the SSH certificate has the required extensions to // fulfill the device trust configuration. -func VerifySSHUser(dt *types.DeviceTrust, cert *ssh.Certificate) error { +func VerifySSHUser(ctx context.Context, dt *types.DeviceTrust, cert *ssh.Certificate) error { if cert == nil { return trace.BadParameter("cert required") } username := cert.KeyId - return verifyDeviceExtensions(dt, username, IsSSHDeviceVerified(cert)) + return verifyDeviceExtensions(ctx, dt, username, IsSSHDeviceVerified(cert)) } -func verifyDeviceExtensions(dt *types.DeviceTrust, username string, verified bool) error { +func verifyDeviceExtensions(ctx context.Context, dt *types.DeviceTrust, username string, verified bool) error { mode := dtconfig.GetEnforcementMode(dt) switch { case mode != constants.DeviceTrustModeRequired: return nil // OK, extensions not enforced. case !verified: - log. - WithField("User", username). - Debug("Device Trust: denied access for unidentified device") + slog.DebugContext(ctx, "Device Trust: denied access for unidentified device", "user", username) return trace.Wrap(ErrTrustedDeviceRequired) default: return nil diff --git a/lib/devicetrust/authz/authz_test.go b/lib/devicetrust/authz/authz_test.go index fa5a415508581..511a6be820d70 100644 --- a/lib/devicetrust/authz/authz_test.go +++ b/lib/devicetrust/authz/authz_test.go @@ -19,6 +19,7 @@ package authz_test import ( + "context" "testing" "github.com/gravitational/trace" @@ -131,7 +132,7 @@ func testIsDeviceVerified(t *testing.T, name string, fn func(ext *tlsca.DeviceEx func TestVerifyTLSUser(t *testing.T) { runVerifyUserTest(t, "VerifyTLSUser", func(dt *types.DeviceTrust, ext *tlsca.DeviceExtensions) error { - return authz.VerifyTLSUser(dt, tlsca.Identity{ + return authz.VerifyTLSUser(context.Background(), dt, tlsca.Identity{ Username: "llama", DeviceExtensions: *ext, }) @@ -140,7 +141,7 @@ func TestVerifyTLSUser(t *testing.T) { func TestVerifySSHUser(t *testing.T) { runVerifyUserTest(t, "VerifySSHUser", func(dt *types.DeviceTrust, ext *tlsca.DeviceExtensions) error { - return authz.VerifySSHUser(dt, &ssh.Certificate{ + return authz.VerifySSHUser(context.Background(), dt, &ssh.Certificate{ KeyId: "llama", Permissions: ssh.Permissions{ Extensions: map[string]string{ diff --git a/lib/devicetrust/enroll/enroll.go b/lib/devicetrust/enroll/enroll.go index b7e8a18c662eb..bdf3cea00c1b4 100644 --- a/lib/devicetrust/enroll/enroll.go +++ b/lib/devicetrust/enroll/enroll.go @@ -22,10 +22,10 @@ import ( "context" "errors" "io" + "log/slog" "github.com/gravitational/trace" "github.com/gravitational/trace/trail" - log "github.com/sirupsen/logrus" devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1" "github.com/gravitational/teleport/lib/devicetrust" @@ -94,8 +94,7 @@ func (c *Ceremony) RunAdmin( rewordAccessDenied := func(err error, action string) error { if trace.IsAccessDenied(trail.FromGRPC(err)) { - log.WithError(err).Debug( - "Device Trust: Redacting access denied error with user-friendly message") + slog.DebugContext(ctx, "Device Trust: Redacting access denied error with user-friendly message", "error", err) return trace.AccessDenied( "User does not have permissions to %s. Contact your cluster device administrator.", action, @@ -115,9 +114,12 @@ func (c *Ceremony) RunAdmin( for _, dev := range findResp.Devices { if dev.OsType == osType { currentDev = dev - log.Debugf( - "Device Trust: Found device %q/%v, id=%q", - currentDev.AssetTag, devicetrust.FriendlyOSType(currentDev.OsType), currentDev.Id, + slog.DebugContext(ctx, "Device Trust: Found device", + slog.Group("device", + slog.String("asset_tag", currentDev.AssetTag), + slog.String("os", devicetrust.FriendlyOSType(currentDev.OsType)), + slog.String("id", currentDev.Id), + ), ) break } @@ -148,10 +150,13 @@ func (c *Ceremony) RunAdmin( if err != nil { return currentDev, outcome, trace.Wrap(rewordAccessDenied(err, "create device enrollment tokens")) } - log.Debugf( - "Device Trust: Created enrollment token for device %q/%s", - currentDev.AssetTag, - devicetrust.FriendlyOSType(currentDev.OsType)) + slog.DebugContext(ctx, "Device Trust: Created enrollment token for device", + slog.Group("device", + slog.String("asset_tag", currentDev.AssetTag), + slog.String("os", devicetrust.FriendlyOSType(currentDev.OsType)), + slog.String("id", currentDev.Id), + ), + ) } token := currentDev.EnrollToken.GetToken() diff --git a/lib/devicetrust/errors.go b/lib/devicetrust/errors.go index 4ca5e7c41107a..b2df583f57f14 100644 --- a/lib/devicetrust/errors.go +++ b/lib/devicetrust/errors.go @@ -19,11 +19,12 @@ package devicetrust import ( + "context" "errors" "io" + "log/slog" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -48,14 +49,14 @@ func HandleUnimplemented(err error) error { const notSupportedMsg = "device trust not supported by remote cluster" if errors.Is(err, io.EOF) { - log.Debug("Device Trust: interpreting EOF as an older Teleport cluster") + slog.DebugContext(context.Background(), "Device Trust: interpreting EOF as an older Teleport cluster") return trace.NotImplemented(notSupportedMsg) } for e := err; e != nil; { switch s, ok := status.FromError(e); { case ok && s.Code() == codes.Unimplemented: - log.WithError(err).Debug("Device Trust: interpreting gRPC Unimplemented as OSS or older Enterprise cluster") + slog.DebugContext(context.Background(), "Device Trust: interpreting gRPC Unimplemented as OSS or older Enterprise cluster", "error", err) return trace.NotImplemented(notSupportedMsg) case ok: return err // Unexpected status error. diff --git a/lib/devicetrust/native/api.go b/lib/devicetrust/native/api.go index 87cab17649979..ad13f1189da54 100644 --- a/lib/devicetrust/native/api.go +++ b/lib/devicetrust/native/api.go @@ -19,12 +19,13 @@ package native import ( + "context" + "log/slog" "runtime" "sync" "time" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -88,7 +89,7 @@ func readCachedDeviceDataUnderLock(mode CollectDataMode) (cdd *devicepb.DeviceCo return nil, false } - log.Debug("Device Trust: Using in-process cached device data") + slog.DebugContext(context.Background(), "Device Trust: Using in-process cached device data") cdd = proto.Clone(cachedDeviceData.value).(*devicepb.DeviceCollectedData) cdd.CollectTime = timestamppb.Now() return cdd, true diff --git a/lib/devicetrust/native/device_darwin.go b/lib/devicetrust/native/device_darwin.go index 9167b567aec9a..b3776ccd7e221 100644 --- a/lib/devicetrust/native/device_darwin.go +++ b/lib/devicetrust/native/device_darwin.go @@ -27,11 +27,13 @@ import "C" import ( "bytes" + "context" "crypto/sha256" "crypto/x509" "errors" "fmt" "io/fs" + "log/slog" "os/exec" "os/user" "strings" @@ -40,7 +42,6 @@ import ( "github.com/google/uuid" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/timestamppb" devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1" @@ -139,7 +140,7 @@ func collectDeviceData(_ CollectDataMode) (*devicepb.DeviceCollectedData, error) defer wg.Done() out, err := spec.fn() if err != nil { - log.WithError(err).Warnf("Device Trust: Failed to get %v", spec.desc) + slog.WarnContext(context.Background(), "Device Trust: Failed to get device details", "details", spec.desc, "error", err) return } *spec.out = out @@ -179,7 +180,7 @@ func getJamfBinaryVersion() (string, error) { // Jamf binary may not exist. This is alright. pathErr := &fs.PathError{} if errors.As(err, &pathErr) { - log.Debugf("Device Trust: Jamf binary not found: %q", pathErr.Path) + slog.DebugContext(context.Background(), "Device Trust: Jamf binary not found", "binary_path", pathErr.Path) return "", nil } diff --git a/lib/devicetrust/native/device_linux.go b/lib/devicetrust/native/device_linux.go index 63b8363c73393..b0771c1bc72b3 100644 --- a/lib/devicetrust/native/device_linux.go +++ b/lib/devicetrust/native/device_linux.go @@ -26,6 +26,7 @@ import ( "fmt" "io" "io/fs" + "log/slog" "os" "os/exec" "os/user" @@ -34,9 +35,9 @@ import ( "github.com/google/go-attestation/attest" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/gravitational/teleport" devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1" "github.com/gravitational/teleport/lib/linux" ) @@ -104,9 +105,10 @@ func rewriteTPMPermissionError(err error) error { if !errors.As(err, &pathErr) || pathErr.Path != "/dev/tpmrm0" { return err } - log. - WithError(err). - Debug("TPM: Replacing TPM permission error with a more friendly one") + slog.DebugContext(context.Background(), "Replacing TPM permission error with a more friendly one", + teleport.ComponentKey, "TPM", + "error", err, + ) return errors.New("" + "Failed to open the TPM device. " + @@ -141,7 +143,10 @@ func collectDeviceData(mode CollectDataMode) (*devicepb.DeviceCollectedData, err go func() { osRelease, err := cddFuncs.parseOSRelease() if err != nil { - log.WithError(err).Debug("TPM: Failed to parse /etc/os-release file") + slog.DebugContext(context.Background(), "Failed to parse /etc/os-release file", + teleport.ComponentKey, "TPM", + "error", err, + ) // err swallowed on purpose. osRelease = &linux.OSRelease{} @@ -187,26 +192,29 @@ func collectDeviceData(mode CollectDataMode) (*devicepb.DeviceCollectedData, err } func readDMIInfoAccordingToMode(mode CollectDataMode) (*linux.DMIInfo, error) { + ctx := context.Background() + logger := slog.With(teleport.ComponentKey, "TPM") + dmiInfo, err := cddFuncs.dmiInfoFromSysfs() if err == nil { return dmiInfo, nil } - log.WithError(err).Warn("TPM: Failed to read device model and/or serial numbers") + logger.WarnContext(ctx, "Failed to read device model and/or serial numbers", "error", err) if !errors.Is(err, fs.ErrPermission) { return dmiInfo, nil // original info } switch mode { case CollectedDataNeverEscalate, CollectedDataMaybeEscalate: - log.Debug("TPM: Reading cached DMI info") + logger.DebugContext(ctx, "Reading cached DMI info") dmiCached, err := cddFuncs.readDMIInfoCached() if err == nil { return dmiCached, nil // successful cache hit } - log.WithError(err).Debug("TPM: Failed to read cached DMI info") + logger.DebugContext(ctx, "Failed to read cached DMI info", "error", err) if mode == CollectedDataNeverEscalate { return dmiInfo, nil // original info } @@ -214,7 +222,7 @@ func readDMIInfoAccordingToMode(mode CollectDataMode) (*linux.DMIInfo, error) { fallthrough case CollectedDataAlwaysEscalate: - log.Debug("TPM: Running escalated `tsh device dmi-info`") + logger.DebugContext(ctx, "Running escalated `tsh device dmi-info`") dmiInfo, err = cddFuncs.readDMIInfoEscalated() if err != nil { @@ -222,7 +230,7 @@ func readDMIInfoAccordingToMode(mode CollectDataMode) (*linux.DMIInfo, error) { } if err := cddFuncs.saveDMIInfoToCache(dmiInfo); err != nil { - log.WithError(err).Warn("TPM: Failed to write DMI cache") + logger.WarnContext(ctx, "Failed to write DMI cache", "error", err) // err swallowed on purpose. } } @@ -250,9 +258,7 @@ func readDMIInfoCached() (*linux.DMIInfo, error) { return nil, trace.Wrap(err) } if dec.More() { - log. - WithField("Path", path). - Warn("DMI cache file contains multiple JSON entries, only one expected") + slog.WarnContext(context.Background(), "DMI cache file contains multiple JSON entries, only one expected", "path", path) // Warn but keep going. } @@ -320,7 +326,7 @@ func saveDMIInfoToCache(dmiInfo *linux.DMIInfo) error { if err := f.Close(); err != nil { return trace.Wrap(err, "closing dmi.json after write") } - log.Debug("TPM: Saved DMI information to local cache") + slog.DebugContext(context.Background(), "Saved DMI information to local cache", teleport.ComponentKey, "TPM") return nil } diff --git a/lib/devicetrust/native/device_linux_test.go b/lib/devicetrust/native/device_linux_test.go index 6f1f545437ccb..7848c6a05767b 100644 --- a/lib/devicetrust/native/device_linux_test.go +++ b/lib/devicetrust/native/device_linux_test.go @@ -28,7 +28,6 @@ import ( "testing" "github.com/google/go-cmp/cmp" - log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" @@ -39,9 +38,6 @@ import ( ) func TestCollectDeviceData_linux(t *testing.T) { - // Silence logging for tests. - log.SetLevel(log.PanicLevel) - // Do not cache data during testing. skipCacheBefore := cachedDeviceData.skipCache cachedDeviceData.skipCache = true diff --git a/lib/devicetrust/native/device_windows.go b/lib/devicetrust/native/device_windows.go index b0a871c18a994..575e238af5623 100644 --- a/lib/devicetrust/native/device_windows.go +++ b/lib/devicetrust/native/device_windows.go @@ -19,17 +19,19 @@ package native import ( + "context" "encoding/base64" "errors" "fmt" + "log/slog" "os" "os/user" "strconv" "time" "github.com/google/go-attestation/attest" + "github.com/gravitational/teleport" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "github.com/yusufpapurcu/wmi" "golang.org/x/sync/errgroup" "golang.org/x/sys/windows" @@ -166,7 +168,10 @@ func getDeviceBaseBoardSerial() (string, error) { } func collectDeviceData(_ CollectDataMode) (*devicepb.DeviceCollectedData, error) { - log.Debug("TPM: Collecting device data.") + ctx := context.Background() + logger := slog.With(teleport.ComponentKey, "TPM") + + logger.DebugContext(ctx, "Collecting device data") var g errgroup.Group const groupLimit = 4 // arbitrary @@ -188,7 +193,7 @@ func collectDeviceData(_ CollectDataMode) (*devicepb.DeviceCollectedData, error) g.Go(func() error { val, err := spec.fn() if err != nil { - log.WithError(err).Debugf("TPM: Failed to fetch %v", spec.desc) + logger.DebugContext(ctx, "Failed to fetch device details", "details", spec.desc, "error", err) return nil // Swallowed on purpose. } @@ -224,9 +229,7 @@ func collectDeviceData(_ CollectDataMode) (*devicepb.DeviceCollectedData, error) BaseBoardSerialNumber: baseBoardSerial, ReportedAssetTag: reportedAssetTag, } - log.WithField( - "device_collected_data", dcd, - ).Debug("TPM: Device data collected.") + logger.DebugContext(ctx, "Device data collected", "device_collected_data", dcd) return dcd, nil } @@ -270,7 +273,7 @@ func activateCredentialInElevatedChild( params = append(params, "--debug") } - log.Debug("Starting elevated process.") + slog.DebugContext(context.Background(), "Starting elevated process.") // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew err = windowsexec.RunAsAndWait( exe, @@ -286,7 +289,7 @@ func activateCredentialInElevatedChild( // it. defer func() { if err := os.Remove(credActivationPath); err != nil { - log.WithError(err).Debug("Failed to clean up credential activation result") + slog.DebugContext(context.Background(), "Failed to clean up credential activation result", "error", err) } }() diff --git a/lib/devicetrust/native/tpm_device.go b/lib/devicetrust/native/tpm_device.go index 112647f456c90..cbfbc1b51012e 100644 --- a/lib/devicetrust/native/tpm_device.go +++ b/lib/devicetrust/native/tpm_device.go @@ -21,18 +21,20 @@ package native import ( + "context" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "fmt" + "log/slog" "math/big" "os" "github.com/google/go-attestation/attest" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" + "github.com/gravitational/teleport" devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1" "github.com/gravitational/teleport/lib/devicetrust" ) @@ -121,6 +123,8 @@ func createAndSaveAK( } func (d *tpmDevice) enrollDeviceInit() (*devicepb.EnrollDeviceInit, error) { + ctx := context.Background() + logger := slog.With(teleport.ComponentKey, "TPM") stateDir, err := setupDeviceStateDir(userDirFunc) if err != nil { return nil, trace.Wrap(err, "setting up device state directory") @@ -134,7 +138,7 @@ func (d *tpmDevice) enrollDeviceInit() (*devicepb.EnrollDeviceInit, error) { } defer func() { if err := tpm.Close(); err != nil { - log.WithError(err).Debug("TPM: Failed to close TPM.") + logger.DebugContext(ctx, "Failed to close TPM", "error", err) } }() @@ -145,13 +149,13 @@ func (d *tpmDevice) enrollDeviceInit() (*devicepb.EnrollDeviceInit, error) { if !trace.IsNotFound(err) { return nil, trace.Wrap(err, "loading ak") } - log.Debug("TPM: No existing AK was found on disk, an AK will be created.") + logger.DebugContext(ctx, "No existing AK was found on disk, an AK will be created") ak, err = createAndSaveAK(tpm, stateDir.attestationKeyPath) if err != nil { return nil, trace.Wrap(err, "creating ak") } } else { - log.Debug("TPM: Existing AK was found on disk, it will be reused.") + logger.DebugContext(ctx, "Existing AK was found on disk, it will be reused") } defer ak.Close(tpm) @@ -245,7 +249,10 @@ func (d *tpmDevice) getDeviceCredential() (*devicepb.DeviceCredential, error) { } defer func() { if err := tpm.Close(); err != nil { - log.WithError(err).Debug("TPM: Failed to close TPM.") + slog.DebugContext(context.Background(), "Failed to close TPM", + teleport.ComponentKey, "TPM", + "error", err, + ) } }() @@ -270,6 +277,9 @@ func (d *tpmDevice) solveTPMEnrollChallenge( challenge *devicepb.TPMEnrollChallenge, debug bool, ) (*devicepb.TPMEnrollChallengeResponse, error) { + ctx := context.Background() + logger := slog.With(teleport.ComponentKey, "TPM") + stateDir, err := setupDeviceStateDir(userDirFunc) if err != nil { return nil, trace.Wrap(err, "setting up device state directory") @@ -283,7 +293,7 @@ func (d *tpmDevice) solveTPMEnrollChallenge( } defer func() { if err := tpm.Close(); err != nil { - log.WithError(err).Debug("TPM: Failed to close TPM.") + logger.DebugContext(ctx, "Failed to close TPM", "error", err) } }() @@ -302,7 +312,7 @@ func (d *tpmDevice) solveTPMEnrollChallenge( // First perform the credential activation challenge provided by the // auth server. - log.Debug("TPM: Activating credential.") + logger.DebugContext(ctx, "Activating credential") encryptedCredential := devicetrust.EncryptedCredentialFromProto( challenge.EncryptedCredential, ) @@ -318,7 +328,7 @@ func (d *tpmDevice) solveTPMEnrollChallenge( var activationSolution []byte if elevated { - log.Debug("TPM: Detected current process is elevated. Will run credential activation in current process.") + logger.DebugContext(ctx, "Detected current process is elevated. Will run credential activation in current process") // If we are running with elevated privileges, we can just complete the // credential activation here. activationSolution, err = ak.ActivateCredential( @@ -341,7 +351,7 @@ func (d *tpmDevice) solveTPMEnrollChallenge( fmt.Fprintln(os.Stderr, "Successfully completed credential activation in elevated process.") } - log.Debug("TPM: Enrollment challenge completed.") + logger.DebugContext(ctx, "Enrollment challenge completed.") return &devicepb.TPMEnrollChallengeResponse{ Solution: activationSolution, PlatformParameters: devicetrust.PlatformParametersToProto( @@ -352,7 +362,10 @@ func (d *tpmDevice) solveTPMEnrollChallenge( //nolint:unused // Used by Windows builds. func (d *tpmDevice) handleTPMActivateCredential(encryptedCredential, encryptedCredentialSecret string) error { - log.Debug("Performing credential activation.") + ctx := context.Background() + logger := slog.With(teleport.ComponentKey, "TPM") + + logger.DebugContext(ctx, "Performing credential activation") // The two input parameters are base64 encoded, so decode them. credentialBytes, err := base64.StdEncoding.DecodeString(encryptedCredential) if err != nil { @@ -376,7 +389,7 @@ func (d *tpmDevice) handleTPMActivateCredential(encryptedCredential, encryptedCr } defer func() { if err := tpm.Close(); err != nil { - log.WithError(err).Debug("TPM: Failed to close TPM.") + logger.DebugContext(ctx, "Failed to close TPM", "error", err) } }() @@ -398,7 +411,7 @@ func (d *tpmDevice) handleTPMActivateCredential(encryptedCredential, encryptedCr return trace.Wrap(err, "activating credential with challenge") } - log.Debug("Completed credential activation. Returning result to original process.") + logger.DebugContext(ctx, "Completed credential activation, returning result to original process") return trace.Wrap( os.WriteFile(stateDir.credentialActivationPath, solution, 0600), ) @@ -407,6 +420,9 @@ func (d *tpmDevice) handleTPMActivateCredential(encryptedCredential, encryptedCr func (d *tpmDevice) solveTPMAuthnDeviceChallenge( challenge *devicepb.TPMAuthenticateDeviceChallenge, ) (*devicepb.TPMAuthenticateDeviceChallengeResponse, error) { + ctx := context.Background() + logger := slog.With(teleport.ComponentKey, "TPM") + stateDir, err := setupDeviceStateDir(userDirFunc) if err != nil { return nil, trace.Wrap(err, "setting up device state directory") @@ -420,7 +436,7 @@ func (d *tpmDevice) solveTPMAuthnDeviceChallenge( } defer func() { if err := tpm.Close(); err != nil { - log.WithError(err).Debug("TPM: Failed to close TPM") + logger.DebugContext(ctx, "Failed to close TPM", "error", err) } }() @@ -437,7 +453,7 @@ func (d *tpmDevice) solveTPMAuthnDeviceChallenge( return nil, trace.Wrap(err) } - log.Debug("TPM: Authenticate device challenge completed.") + logger.DebugContext(ctx, "Authenticate device challenge completed") return &devicepb.TPMAuthenticateDeviceChallengeResponse{ PlatformParameters: devicetrust.PlatformParametersToProto( platformsParams, @@ -446,9 +462,12 @@ func (d *tpmDevice) solveTPMAuthnDeviceChallenge( } func attestPlatform(tpm *attest.TPM, ak *attest.AK, nonce []byte) (*attest.PlatformParameters, error) { + ctx := context.Background() + logger := slog.With(teleport.ComponentKey, "TPM") + config := &attest.PlatformAttestConfig{} - log.Debug("TPM: Performing platform attestation.") + logger.DebugContext(ctx, "Performing platform attestation") platformsParams, err := tpm.AttestPlatform(ak, nonce, config) if err == nil { return platformsParams, nil @@ -458,9 +477,7 @@ func attestPlatform(tpm *attest.TPM, ak *attest.AK, nonce []byte) (*attest.Platf // errors.Is(err, fs.ErrPermission), but the go-attestation version at time of // writing (v0.5.0) doesn't wrap the underlying error. // This is a common occurrence for Linux devices. - log. - WithError(err). - Debug("TPM: Platform attestation failed with permission error, attempting without event log") + logger.DebugContext(ctx, "Platform attestation failed with permission error, attempting without event log", "error", err) config.EventLog = []byte{} platformsParams, err = tpm.AttestPlatform(ak, nonce, config) return platformsParams, trace.Wrap(err, "attesting platform") diff --git a/lib/srv/session_control.go b/lib/srv/session_control.go index 748aa111062eb..536cfd8948ff2 100644 --- a/lib/srv/session_control.go +++ b/lib/srv/session_control.go @@ -236,7 +236,7 @@ func (s *SessionController) AcquireSessionContext(ctx context.Context, identity } // Device Trust: authorize device extensions. - if err := dtauthz.VerifySSHUser(authPref.GetDeviceTrust(), identity.Certificate); err != nil { + if err := dtauthz.VerifySSHUser(ctx, authPref.GetDeviceTrust(), identity.Certificate); err != nil { return ctx, trace.Wrap(err) } From aaea04786446a0ef5f79b3812afb5843478f906e Mon Sep 17 00:00:00 2001 From: Hugo Shaka Date: Thu, 19 Dec 2024 18:36:38 -0500 Subject: [PATCH 29/64] set rollout start date and don't start updating if rollout just changed (#50365) This commit does two changes: - the controller now sets the rollout start time when resetting the rollout - the controller will not start a group if the rollout changed during the maintenance window (checks if the rollout start time is in the window) --- .../teleport/autoupdate/v1/autoupdate.pb.go | 1 + .../teleport/autoupdate/v1/autoupdate.proto | 1 + lib/autoupdate/rollout/controller.go | 1 + lib/autoupdate/rollout/reconciler.go | 12 ++-- lib/autoupdate/rollout/reconciler_test.go | 57 ++++++++++------ lib/autoupdate/rollout/strategy.go | 13 +++- .../rollout/strategy_haltonerror.go | 39 +++++++---- .../rollout/strategy_haltonerror_test.go | 38 +++++++++-- lib/autoupdate/rollout/strategy_test.go | 65 +++++++++++++++++-- lib/autoupdate/rollout/strategy_timebased.go | 22 +++++-- .../rollout/strategy_timebased_test.go | 40 ++++++++++-- 11 files changed, 232 insertions(+), 57 deletions(-) diff --git a/api/gen/proto/go/teleport/autoupdate/v1/autoupdate.pb.go b/api/gen/proto/go/teleport/autoupdate/v1/autoupdate.pb.go index 7fc41ec3f3858..8d7e52517814b 100644 --- a/api/gen/proto/go/teleport/autoupdate/v1/autoupdate.pb.go +++ b/api/gen/proto/go/teleport/autoupdate/v1/autoupdate.pb.go @@ -997,6 +997,7 @@ type AutoUpdateAgentRolloutStatus struct { // For example, a group updates every day between 13:00 and 14:00. If the target version changes to 13:30, the group // will not start updating to the new version directly. The controller sees that the group theoretical start time is // before the rollout start time and the maintenance window belongs to the previous rollout. + // When the timestamp is nil, the controller will ignore the start time and check and allow groups to activate. StartTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/api/proto/teleport/autoupdate/v1/autoupdate.proto b/api/proto/teleport/autoupdate/v1/autoupdate.proto index 0257ec3e023b7..faf7ec93db129 100644 --- a/api/proto/teleport/autoupdate/v1/autoupdate.proto +++ b/api/proto/teleport/autoupdate/v1/autoupdate.proto @@ -180,6 +180,7 @@ message AutoUpdateAgentRolloutStatus { // For example, a group updates every day between 13:00 and 14:00. If the target version changes to 13:30, the group // will not start updating to the new version directly. The controller sees that the group theoretical start time is // before the rollout start time and the maintenance window belongs to the previous rollout. + // When the timestamp is nil, the controller will ignore the start time and check and allow groups to activate. google.protobuf.Timestamp start_time = 3; } diff --git a/lib/autoupdate/rollout/controller.go b/lib/autoupdate/rollout/controller.go index 1c45a6ac4fc5b..eb253f366b6a7 100644 --- a/lib/autoupdate/rollout/controller.go +++ b/lib/autoupdate/rollout/controller.go @@ -71,6 +71,7 @@ func NewController(client Client, log *slog.Logger, clock clockwork.Clock, perio reconciler: reconciler{ clt: client, log: log, + clock: clock, rolloutStrategies: []rolloutStrategy{ // TODO(hugoShaka): add the strategies here as we implement them }, diff --git a/lib/autoupdate/rollout/reconciler.go b/lib/autoupdate/rollout/reconciler.go index 1e186156a9daa..a6264ba3cbf27 100644 --- a/lib/autoupdate/rollout/reconciler.go +++ b/lib/autoupdate/rollout/reconciler.go @@ -153,7 +153,7 @@ func (r *reconciler) tryReconcile(ctx context.Context) error { return trace.Wrap(err, "computing rollout status") } - // there was an existing rollout, we must figure if something changed + // We compute if something changed. specChanged := !proto.Equal(existingRollout.GetSpec(), newSpec) statusChanged := !proto.Equal(existingRollout.GetStatus(), newStatus) rolloutChanged := specChanged || statusChanged @@ -273,6 +273,8 @@ func (r *reconciler) computeStatus( // We create a new status if the rollout should be reset or the previous status was nil if shouldResetRollout || existingRollout.GetStatus() == nil { status = new(autoupdate.AutoUpdateAgentRolloutStatus) + // We set the start time if this is a new rollout + status.StartTime = timestamppb.New(r.clock.Now()) } else { status = utils.CloneProtoMsg(existingRollout.GetStatus()) } @@ -302,8 +304,9 @@ func (r *reconciler) computeStatus( return nil, trace.Wrap(err, "creating groups status") } } + status.Groups = groups - err = r.progressRollout(ctx, newSpec.GetStrategy(), groups) + err = r.progressRollout(ctx, newSpec.GetStrategy(), status) // Failing to progress the update is not a hard failure. // We want to update the status even if something went wrong to surface the failed reconciliation and potential errors to the user. if err != nil { @@ -311,7 +314,6 @@ func (r *reconciler) computeStatus( "error", err) } - status.Groups = groups status.State = computeRolloutState(groups) return status, nil } @@ -320,10 +322,10 @@ func (r *reconciler) computeStatus( // groups are updated in place. // If an error is returned, the groups should still be upserted, depending on the strategy, // failing to update a group might not be fatal (other groups can still progress independently). -func (r *reconciler) progressRollout(ctx context.Context, strategyName string, groups []*autoupdate.AutoUpdateAgentRolloutStatusGroup) error { +func (r *reconciler) progressRollout(ctx context.Context, strategyName string, status *autoupdate.AutoUpdateAgentRolloutStatus) error { for _, strategy := range r.rolloutStrategies { if strategy.name() == strategyName { - return strategy.progressRollout(ctx, groups) + return strategy.progressRollout(ctx, status) } } return trace.NotImplemented("rollout strategy %q not implemented", strategyName) diff --git a/lib/autoupdate/rollout/reconciler_test.go b/lib/autoupdate/rollout/reconciler_test.go index 9518fe66280c2..b5b77cd735edb 100644 --- a/lib/autoupdate/rollout/reconciler_test.go +++ b/lib/autoupdate/rollout/reconciler_test.go @@ -139,6 +139,8 @@ func TestTryReconcile(t *testing.T) { t.Parallel() log := utils.NewSlogLoggerForTests() ctx := context.Background() + clock := clockwork.NewFakeClock() + // Test setup: creating fixtures configOK, err := update.NewAutoUpdateConfig(&autoupdate.AutoUpdateConfigSpec{ Tools: &autoupdate.AutoUpdateConfigSpecTools{ @@ -186,7 +188,7 @@ func TestTryReconcile(t *testing.T) { Strategy: update.AgentsStrategyHaltOnError, }) require.NoError(t, err) - upToDateRollout.Status = &autoupdate.AutoUpdateAgentRolloutStatus{} + upToDateRollout.Status = &autoupdate.AutoUpdateAgentRolloutStatus{StartTime: timestamppb.New(clock.Now())} outOfDateRollout, err := update.NewAutoUpdateAgentRollout(&autoupdate.AutoUpdateAgentRolloutSpec{ StartVersion: "1.2.2", @@ -315,8 +317,9 @@ func TestTryReconcile(t *testing.T) { // Test execution: Running the reconciliation reconciler := &reconciler{ - clt: client, - log: log, + clt: client, + log: log, + clock: clock, } require.NoError(t, reconciler.tryReconcile(ctx)) @@ -330,6 +333,7 @@ func TestTryReconcile(t *testing.T) { func TestReconciler_Reconcile(t *testing.T) { log := utils.NewSlogLoggerForTests() ctx := context.Background() + clock := clockwork.NewFakeClock() // Test setup: creating fixtures config, err := update.NewAutoUpdateConfig(&autoupdate.AutoUpdateConfigSpec{ Tools: &autoupdate.AutoUpdateConfigSpecTools{ @@ -361,7 +365,7 @@ func TestReconciler_Reconcile(t *testing.T) { Strategy: update.AgentsStrategyHaltOnError, }) require.NoError(t, err) - upToDateRollout.Status = &autoupdate.AutoUpdateAgentRolloutStatus{} + upToDateRollout.Status = &autoupdate.AutoUpdateAgentRolloutStatus{StartTime: timestamppb.New(clock.Now())} outOfDateRollout, err := update.NewAutoUpdateAgentRollout(&autoupdate.AutoUpdateAgentRolloutSpec{ StartVersion: "1.2.2", @@ -407,8 +411,9 @@ func TestReconciler_Reconcile(t *testing.T) { client := newMockClient(t, stubs) reconciler := &reconciler{ - clt: client, - log: log, + clt: client, + log: log, + clock: clock, } // Test execution: run the reconciliation loop @@ -431,8 +436,9 @@ func TestReconciler_Reconcile(t *testing.T) { client := newMockClient(t, stubs) reconciler := &reconciler{ - clt: client, - log: log, + clt: client, + log: log, + clock: clock, } // Test execution: run the reconciliation loop @@ -471,8 +477,9 @@ func TestReconciler_Reconcile(t *testing.T) { client := newMockClient(t, stubs) reconciler := &reconciler{ - clt: client, - log: log, + clt: client, + log: log, + clock: clock, } // Test execution: run the reconciliation loop @@ -509,8 +516,9 @@ func TestReconciler_Reconcile(t *testing.T) { client := newMockClient(t, stubs) reconciler := &reconciler{ - clt: client, - log: log, + clt: client, + log: log, + clock: clock, } // Test execution: run the reconciliation loop @@ -533,8 +541,9 @@ func TestReconciler_Reconcile(t *testing.T) { client := newMockClient(t, stubs) reconciler := &reconciler{ - clt: client, - log: log, + clt: client, + log: log, + clock: clock, } // Test execution: run the reconciliation loop @@ -563,8 +572,9 @@ func TestReconciler_Reconcile(t *testing.T) { client := newMockClient(t, stubs) reconciler := &reconciler{ - clt: client, - log: log, + clt: client, + log: log, + clock: clock, } // Test execution: run the reconciliation loop @@ -704,7 +714,7 @@ func (f *fakeRolloutStrategy) name() string { return f.strategyName } -func (f *fakeRolloutStrategy) progressRollout(ctx context.Context, groups []*autoupdate.AutoUpdateAgentRolloutStatusGroup) error { +func (f *fakeRolloutStrategy) progressRollout(ctx context.Context, status *autoupdate.AutoUpdateAgentRolloutStatus) error { f.calls++ return nil } @@ -742,8 +752,9 @@ func Test_reconciler_computeStatus(t *testing.T) { newGroups, err := r.makeGroupsStatus(ctx, schedules, clock.Now()) require.NoError(t, err) newStatus := &autoupdate.AutoUpdateAgentRolloutStatus{ - Groups: newGroups, - State: autoupdate.AutoUpdateAgentRolloutState_AUTO_UPDATE_AGENT_ROLLOUT_STATE_UNSTARTED, + Groups: newGroups, + State: autoupdate.AutoUpdateAgentRolloutState_AUTO_UPDATE_AGENT_ROLLOUT_STATE_UNSTARTED, + StartTime: timestamppb.New(clock.Now()), } tests := []struct { @@ -835,7 +846,9 @@ func Test_reconciler_computeStatus(t *testing.T) { Strategy: fakeRolloutStrategyName, }, // groups should be unset - expectedStatus: &autoupdate.AutoUpdateAgentRolloutStatus{}, + expectedStatus: &autoupdate.AutoUpdateAgentRolloutStatus{ + StartTime: timestamppb.New(clock.Now()), + }, expectedStrategyCalls: 0, }, { @@ -843,7 +856,9 @@ func Test_reconciler_computeStatus(t *testing.T) { existingRollout: &autoupdate.AutoUpdateAgentRollout{ Spec: oldSpec, // old groups were empty - Status: &autoupdate.AutoUpdateAgentRolloutStatus{}, + Status: &autoupdate.AutoUpdateAgentRolloutStatus{ + StartTime: timestamppb.New(clock.Now()), + }, }, // no spec change newSpec: oldSpec, diff --git a/lib/autoupdate/rollout/strategy.go b/lib/autoupdate/rollout/strategy.go index 025d6ae01570d..b3e214f6cebd7 100644 --- a/lib/autoupdate/rollout/strategy.go +++ b/lib/autoupdate/rollout/strategy.go @@ -33,13 +33,14 @@ const ( // Common update reasons updateReasonCreated = "created" updateReasonReconcilerError = "reconciler_error" + updateReasonRolloutChanged = "rollout_changed_during_window" ) // rolloutStrategy is responsible for rolling out the update across groups. // This interface allows us to inject dummy strategies for simpler testing. type rolloutStrategy interface { name() string - progressRollout(context.Context, []*autoupdate.AutoUpdateAgentRolloutStatusGroup) error + progressRollout(context.Context, *autoupdate.AutoUpdateAgentRolloutStatus) error } func inWindow(group *autoupdate.AutoUpdateAgentRolloutStatusGroup, now time.Time) (bool, error) { @@ -53,6 +54,16 @@ func inWindow(group *autoupdate.AutoUpdateAgentRolloutStatusGroup, now time.Time return int(group.ConfigStartHour) == now.Hour(), nil } +// rolloutChangedInWindow checks if the rollout got created after the theoretical group start time +func rolloutChangedInWindow(group *autoupdate.AutoUpdateAgentRolloutStatusGroup, now, rolloutStart time.Time) (bool, error) { + // If the rollout is older than 24h, we know it did not change during the window + if now.Sub(rolloutStart) > 24*time.Hour { + return false, nil + } + // Else we check if the rollout happened in the group window. + return inWindow(group, rolloutStart) +} + func canUpdateToday(allowedDays []string, now time.Time) (bool, error) { for _, allowedDay := range allowedDays { if allowedDay == types.Wildcard { diff --git a/lib/autoupdate/rollout/strategy_haltonerror.go b/lib/autoupdate/rollout/strategy_haltonerror.go index c93438aaa1941..0ab57a052768d 100644 --- a/lib/autoupdate/rollout/strategy_haltonerror.go +++ b/lib/autoupdate/rollout/strategy_haltonerror.go @@ -60,7 +60,7 @@ func newHaltOnErrorStrategy(log *slog.Logger, clock clockwork.Clock) (rolloutStr }, nil } -func (h *haltOnErrorStrategy) progressRollout(ctx context.Context, groups []*autoupdate.AutoUpdateAgentRolloutStatusGroup) error { +func (h *haltOnErrorStrategy) progressRollout(ctx context.Context, status *autoupdate.AutoUpdateAgentRolloutStatus) error { now := h.clock.Now() // We process every group in order, all the previous groups must be in the DONE state // for the next group to become active. Even if some early groups are not DONE, @@ -72,12 +72,12 @@ func (h *haltOnErrorStrategy) progressRollout(ctx context.Context, groups []*aut // to transition "staging" to DONE. previousGroupsAreDone := true - for i, group := range groups { + for i, group := range status.Groups { switch group.State { case autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED: var previousGroup *autoupdate.AutoUpdateAgentRolloutStatusGroup if i != 0 { - previousGroup = groups[i-1] + previousGroup = status.Groups[i-1] } canStart, err := canStartHaltOnError(group, previousGroup, now) if err != nil { @@ -86,16 +86,31 @@ func (h *haltOnErrorStrategy) progressRollout(ctx context.Context, groups []*aut setGroupState(group, group.State, updateReasonReconcilerError, now) return err } + + // Check if the rollout got created after the theoretical group start time + rolloutChangedDuringWindow, err := rolloutChangedInWindow(group, now, status.StartTime.AsTime()) + if err != nil { + setGroupState(group, group.State, updateReasonReconcilerError, now) + return err + } + switch { - case previousGroupsAreDone && canStart: - // We can start - setGroupState(group, autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, updateReasonCanStart, now) - case previousGroupsAreDone: - // All previous groups are OK, but time-related criteria are not OK + case !previousGroupsAreDone: + // All previous groups are not DONE + setGroupState(group, group.State, updateReasonPreviousGroupsNotDone, now) + case !canStart: + // All previous groups are DONE, but time-related criteria are not met + // This can be because we are outside an update window, or because the + // specified wait_hours doesn't let us update yet. setGroupState(group, group.State, updateReasonCannotStart, now) + case rolloutChangedDuringWindow: + // All previous groups are DONE and time-related criteria are met. + // However, the rollout changed during the maintenance window. + setGroupState(group, group.State, updateReasonRolloutChanged, now) default: - // At least one previous group is not DONE - setGroupState(group, group.State, updateReasonPreviousGroupsNotDone, now) + // All previous groups are DONE and time-related criteria are met. + // We can start. + setGroupState(group, autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, updateReasonCanStart, now) } previousGroupsAreDone = false case autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK: @@ -132,10 +147,10 @@ func canStartHaltOnError(group, previousGroup *autoupdate.AutoUpdateAgentRollout previousStart := previousGroup.StartTime.AsTime() if previousStart.IsZero() || previousStart.Unix() == 0 { - return false, trace.BadParameter("the previous group doesn't have a start time, cannot check the 'wait_hours' criteria") + return false, trace.BadParameter("the previous group doesn't have a start time, cannot check the 'wait_hours' criterion") } - // Check if the wait_hours criteria is OK, if we are at least after 'wait_hours' hours since the previous start. + // Check if the wait_hours criterion is OK, if we are at least after 'wait_hours' hours since the previous start. if now.Before(previousGroup.StartTime.AsTime().Add(time.Duration(group.ConfigWaitHours) * time.Hour)) { return false, nil } diff --git a/lib/autoupdate/rollout/strategy_haltonerror_test.go b/lib/autoupdate/rollout/strategy_haltonerror_test.go index 71a653c760361..84d4de069efe3 100644 --- a/lib/autoupdate/rollout/strategy_haltonerror_test.go +++ b/lib/autoupdate/rollout/strategy_haltonerror_test.go @@ -148,9 +148,10 @@ func Test_progressGroupsHaltOnError(t *testing.T) { group3Name := "group3" tests := []struct { - name string - initialState []*autoupdate.AutoUpdateAgentRolloutStatusGroup - expectedState []*autoupdate.AutoUpdateAgentRolloutStatusGroup + name string + initialState []*autoupdate.AutoUpdateAgentRolloutStatusGroup + rolloutStartTime *timestamppb.Timestamp + expectedState []*autoupdate.AutoUpdateAgentRolloutStatusGroup }{ { name: "single group unstarted -> unstarted", @@ -175,6 +176,30 @@ func Test_progressGroupsHaltOnError(t *testing.T) { }, }, }, + { + name: "single group unstarted -> unstarted because rollout changed in window", + initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: group1Name, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED, + LastUpdateTime: timestamppb.New(yesterday), + LastUpdateReason: updateReasonCreated, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + rolloutStartTime: timestamppb.New(clock.Now()), + expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: group1Name, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED, + LastUpdateTime: timestamppb.New(clock.Now()), + LastUpdateReason: updateReasonRolloutChanged, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + }, { name: "single group unstarted -> active", initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ @@ -470,7 +495,12 @@ func Test_progressGroupsHaltOnError(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := strategy.progressRollout(ctx, tt.initialState) + status := &autoupdate.AutoUpdateAgentRolloutStatus{ + Groups: tt.initialState, + State: 0, + StartTime: tt.rolloutStartTime, + } + err := strategy.progressRollout(ctx, status) require.NoError(t, err) // We use require.Equal instead of Elements match because group order matters. // It's not super important for time-based, but is crucial for halt-on-error. diff --git a/lib/autoupdate/rollout/strategy_test.go b/lib/autoupdate/rollout/strategy_test.go index fbb7ec768d644..1348716ba6c1d 100644 --- a/lib/autoupdate/rollout/strategy_test.go +++ b/lib/autoupdate/rollout/strategy_test.go @@ -151,13 +151,70 @@ func Test_inWindow(t *testing.T) { } } +func Test_rolloutChangedInWindow(t *testing.T) { + // Test setup: creating fixtures. + group := &autoupdate.AutoUpdateAgentRolloutStatusGroup{ + Name: "test-group", + ConfigDays: everyWeekdayButSunday, + ConfigStartHour: 12, + } + tests := []struct { + name string + now time.Time + rolloutStart time.Time + want bool + }{ + { + name: "zero rollout start time", + now: testSaturday, + rolloutStart: time.Time{}, + want: false, + }, + { + name: "epoch rollout start time", + now: testSaturday, + // tspb counts since epoch, wile go's zero is 0000-00-00 00:00:00 UTC + rolloutStart: (×tamppb.Timestamp{}).AsTime(), + want: false, + }, + { + name: "rollout changed a week ago", + now: testSaturday, + rolloutStart: testSaturday.Add(-7 * 24 * time.Hour), + want: false, + }, + { + name: "rollout changed the same day, before the window", + now: testSaturday, + rolloutStart: testSaturday.Add(-2 * time.Hour), + want: false, + }, + { + name: "rollout changed the same day, during the window", + now: testSaturday, + rolloutStart: testSaturday.Add(-2 * time.Minute), + want: true, + }, + { + name: "rollout just changed but we are not in a window", + now: testSunday, + rolloutStart: testSunday.Add(-2 * time.Minute), + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test execution. + result, err := rolloutChangedInWindow(group, tt.now, tt.rolloutStart) + require.NoError(t, err) + require.Equal(t, tt.want, result) + }) + } +} + func Test_setGroupState(t *testing.T) { groupName := "test-group" - // TODO(hugoShaka) remove those two variables once the strategies are merged and the constants are defined. - updateReasonCanStart := "can_start" - updateReasonCannotStart := "cannot_start" - clock := clockwork.NewFakeClock() // oldUpdateTime is 5 minutes in the past oldUpdateTime := clock.Now() diff --git a/lib/autoupdate/rollout/strategy_timebased.go b/lib/autoupdate/rollout/strategy_timebased.go index c5abc34be5588..13e844f0e4a5a 100644 --- a/lib/autoupdate/rollout/strategy_timebased.go +++ b/lib/autoupdate/rollout/strategy_timebased.go @@ -56,11 +56,11 @@ func newTimeBasedStrategy(log *slog.Logger, clock clockwork.Clock) (rolloutStrat }, nil } -func (h *timeBasedStrategy) progressRollout(ctx context.Context, groups []*autoupdate.AutoUpdateAgentRolloutStatusGroup) error { +func (h *timeBasedStrategy) progressRollout(ctx context.Context, status *autoupdate.AutoUpdateAgentRolloutStatus) error { now := h.clock.Now() // We always process every group regardless of the order. var errs []error - for _, group := range groups { + for _, group := range status.Groups { switch group.State { case autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED, autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE: @@ -76,10 +76,22 @@ func (h *timeBasedStrategy) progressRollout(ctx context.Context, groups []*autou errs = append(errs, err) continue } - if shouldBeActive { - setGroupState(group, autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, updateReasonInWindow, now) - } else { + + // Check if the rollout got created after the theoretical group start time + rolloutChangedDuringWindow, err := rolloutChangedInWindow(group, now, status.StartTime.AsTime()) + if err != nil { + setGroupState(group, group.State, updateReasonReconcilerError, now) + errs = append(errs, err) + continue + } + + switch { + case !shouldBeActive: setGroupState(group, group.State, updateReasonOutsideWindow, now) + case rolloutChangedDuringWindow: + setGroupState(group, group.State, updateReasonRolloutChanged, now) + default: + setGroupState(group, autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, updateReasonInWindow, now) } case autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK: // We don't touch any group that was manually rolled back. diff --git a/lib/autoupdate/rollout/strategy_timebased_test.go b/lib/autoupdate/rollout/strategy_timebased_test.go index 91db29d42e469..6402da3b21c44 100644 --- a/lib/autoupdate/rollout/strategy_timebased_test.go +++ b/lib/autoupdate/rollout/strategy_timebased_test.go @@ -44,9 +44,10 @@ func Test_progressGroupsTimeBased(t *testing.T) { ctx := context.Background() tests := []struct { - name string - initialState []*autoupdate.AutoUpdateAgentRolloutStatusGroup - expectedState []*autoupdate.AutoUpdateAgentRolloutStatusGroup + name string + initialState []*autoupdate.AutoUpdateAgentRolloutStatusGroup + rolloutStartTime *timestamppb.Timestamp + expectedState []*autoupdate.AutoUpdateAgentRolloutStatusGroup }{ { name: "unstarted -> unstarted", @@ -71,6 +72,30 @@ func Test_progressGroupsTimeBased(t *testing.T) { }, }, }, + { + name: "unstarted -> unstarted because rollout just changed", + initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED, + LastUpdateTime: lastUpdate, + LastUpdateReason: updateReasonCreated, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + rolloutStartTime: timestamppb.New(clock.Now()), + expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED, + LastUpdateTime: timestamppb.New(clock.Now()), + LastUpdateReason: updateReasonRolloutChanged, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + }, { name: "unstarted -> active", initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ @@ -302,13 +327,18 @@ func Test_progressGroupsTimeBased(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := strategy.progressRollout(ctx, tt.initialState) + status := &autoupdate.AutoUpdateAgentRolloutStatus{ + Groups: tt.initialState, + State: 0, + StartTime: tt.rolloutStartTime, + } + err := strategy.progressRollout(ctx, status) require.NoError(t, err) // We use require.Equal instead of Elements match because group order matters. // It's not super important for time-based, but is crucial for halt-on-error. // So it's better to be more conservative and validate order never changes for // both strategies. - require.Equal(t, tt.expectedState, tt.initialState) + require.Equal(t, tt.expectedState, status.Groups) }) } } From e5bf2cea0487e21e97c1e3c9037c01bc25020df3 Mon Sep 17 00:00:00 2001 From: "teleport-post-release-automation[bot]" <128860004+teleport-post-release-automation[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:37:28 +1100 Subject: [PATCH 30/64] [auto] Update AMI IDs for 17.1.0 (#50467) Co-authored-by: GitHub --- assets/aws/Makefile | 2 +- examples/aws/terraform/AMIS.md | 204 +++++++++--------- .../terraform/ha-autoscale-cluster/README.md | 2 +- .../aws/terraform/starter-cluster/README.md | 2 +- 4 files changed, 105 insertions(+), 105 deletions(-) diff --git a/assets/aws/Makefile b/assets/aws/Makefile index ba2ece47b2a23..a13720c8310e3 100644 --- a/assets/aws/Makefile +++ b/assets/aws/Makefile @@ -2,7 +2,7 @@ # This must be a _released_ version of Teleport, i.e. one which has binaries # available for download on https://goteleport.com/download # Unreleased versions will fail to build. -TELEPORT_VERSION ?= 17.0.4 +TELEPORT_VERSION ?= 17.1.0 # Teleport UID is the UID of a non-privileged 'teleport' user TELEPORT_UID ?= 1007 diff --git a/examples/aws/terraform/AMIS.md b/examples/aws/terraform/AMIS.md index 31eddc672bbaa..c9006e85440da 100644 --- a/examples/aws/terraform/AMIS.md +++ b/examples/aws/terraform/AMIS.md @@ -6,116 +6,116 @@ This list is updated when new AMI versions are released. ### OSS ``` -# ap-northeast-1 v17.0.4 arm64 OSS: ami-01716a041218b7583 -# ap-northeast-1 v17.0.4 x86_64 OSS: ami-06a0bce35c7d65bf6 -# ap-northeast-2 v17.0.4 arm64 OSS: ami-02742215c192f5bef -# ap-northeast-2 v17.0.4 x86_64 OSS: ami-0a5e52dbdb5eaa650 -# ap-northeast-3 v17.0.4 arm64 OSS: ami-0160778bf23cf0643 -# ap-northeast-3 v17.0.4 x86_64 OSS: ami-0a7a3595539ec2167 -# ap-south-1 v17.0.4 arm64 OSS: ami-09632d32d2cba70c3 -# ap-south-1 v17.0.4 x86_64 OSS: ami-0ab4d472fa9f1b290 -# ap-southeast-1 v17.0.4 arm64 OSS: ami-0bccbfe98fa25f3d0 -# ap-southeast-1 v17.0.4 x86_64 OSS: ami-01ab2d0af845cd0a7 -# ap-southeast-2 v17.0.4 arm64 OSS: ami-022f9be11a0c74479 -# ap-southeast-2 v17.0.4 x86_64 OSS: ami-04db11a0a7eb447f9 -# ca-central-1 v17.0.4 arm64 OSS: ami-09c09ef2b04cdc0bd -# ca-central-1 v17.0.4 x86_64 OSS: ami-09c4ed011317bd4fc -# eu-central-1 v17.0.4 arm64 OSS: ami-066511d9d4a4b6e37 -# eu-central-1 v17.0.4 x86_64 OSS: ami-004a4d77b5b1dacc2 -# eu-north-1 v17.0.4 arm64 OSS: ami-05cdb6f461101593a -# eu-north-1 v17.0.4 x86_64 OSS: ami-0af9705b2af84685a -# eu-west-1 v17.0.4 arm64 OSS: ami-0e5295d6c18225338 -# eu-west-1 v17.0.4 x86_64 OSS: ami-09f90453fcb9d65e7 -# eu-west-2 v17.0.4 arm64 OSS: ami-070763cd4a0edfc01 -# eu-west-2 v17.0.4 x86_64 OSS: ami-00f9fe423c08672a5 -# eu-west-3 v17.0.4 arm64 OSS: ami-065e2413a3d90fde7 -# eu-west-3 v17.0.4 x86_64 OSS: ami-05809768420d06e55 -# sa-east-1 v17.0.4 arm64 OSS: ami-0d3cc91be3e224009 -# sa-east-1 v17.0.4 x86_64 OSS: ami-0e937294d10077f31 -# us-east-1 v17.0.4 arm64 OSS: ami-0af34c1d6e3070279 -# us-east-1 v17.0.4 x86_64 OSS: ami-05ac65c09e4c769bb -# us-east-2 v17.0.4 arm64 OSS: ami-0404817145aa2b1c5 -# us-east-2 v17.0.4 x86_64 OSS: ami-06e28394d0b853fdc -# us-west-1 v17.0.4 arm64 OSS: ami-03054d783cbd721da -# us-west-1 v17.0.4 x86_64 OSS: ami-038de85609360f42a -# us-west-2 v17.0.4 arm64 OSS: ami-0ead7a03ac47a4aca -# us-west-2 v17.0.4 x86_64 OSS: ami-009e6a010be394578 +# ap-northeast-1 v17.1.0 arm64 OSS: ami-05d48ab4187b69aad +# ap-northeast-1 v17.1.0 x86_64 OSS: ami-0771bb7ea243be53e +# ap-northeast-2 v17.1.0 arm64 OSS: ami-07184d83509a4de9f +# ap-northeast-2 v17.1.0 x86_64 OSS: ami-0f77764082d35f51f +# ap-northeast-3 v17.1.0 arm64 OSS: ami-030b6b0910f582924 +# ap-northeast-3 v17.1.0 x86_64 OSS: ami-0fea7130c366b6660 +# ap-south-1 v17.1.0 arm64 OSS: ami-00536811675fb7ca5 +# ap-south-1 v17.1.0 x86_64 OSS: ami-004c201af536d8c4c +# ap-southeast-1 v17.1.0 arm64 OSS: ami-0512146122ffad85e +# ap-southeast-1 v17.1.0 x86_64 OSS: ami-0a06cfa6a7088c06a +# ap-southeast-2 v17.1.0 arm64 OSS: ami-04ee7db250ee72884 +# ap-southeast-2 v17.1.0 x86_64 OSS: ami-02827e89f41e222c2 +# ca-central-1 v17.1.0 arm64 OSS: ami-0bea774e474572c9a +# ca-central-1 v17.1.0 x86_64 OSS: ami-09b1350d81173ca9f +# eu-central-1 v17.1.0 arm64 OSS: ami-006b6efbc199ba1b7 +# eu-central-1 v17.1.0 x86_64 OSS: ami-0d350bffc56ed6a7e +# eu-north-1 v17.1.0 arm64 OSS: ami-025c9c63e6681a10c +# eu-north-1 v17.1.0 x86_64 OSS: ami-0f757ccc0186db97b +# eu-west-1 v17.1.0 arm64 OSS: ami-0a628413a28dc743e +# eu-west-1 v17.1.0 x86_64 OSS: ami-0c44493ee0f75b08f +# eu-west-2 v17.1.0 arm64 OSS: ami-074beb9d44ee7f8b0 +# eu-west-2 v17.1.0 x86_64 OSS: ami-0526b4f61f6c8ac19 +# eu-west-3 v17.1.0 arm64 OSS: ami-09f0631b07f10f700 +# eu-west-3 v17.1.0 x86_64 OSS: ami-0a3c6bbf9ed4b06fb +# sa-east-1 v17.1.0 arm64 OSS: ami-08e90ce1a60c5d9d7 +# sa-east-1 v17.1.0 x86_64 OSS: ami-0714cd595e4739862 +# us-east-1 v17.1.0 arm64 OSS: ami-060da265f62727bf6 +# us-east-1 v17.1.0 x86_64 OSS: ami-0a481eea172b4293d +# us-east-2 v17.1.0 arm64 OSS: ami-04efea40fcbc97719 +# us-east-2 v17.1.0 x86_64 OSS: ami-0cc8d9116cc499a3c +# us-west-1 v17.1.0 arm64 OSS: ami-063e9e5c822a37abb +# us-west-1 v17.1.0 x86_64 OSS: ami-0f423d0d5bd921fa3 +# us-west-2 v17.1.0 arm64 OSS: ami-073a4498a5bdb3d9f +# us-west-2 v17.1.0 x86_64 OSS: ami-0f091942faaef349a ``` ### Enterprise ``` -# ap-northeast-1 v17.0.4 arm64 Enterprise: ami-073c7d1f9f700ad5f -# ap-northeast-1 v17.0.4 x86_64 Enterprise: ami-080f2379b5915b568 -# ap-northeast-2 v17.0.4 arm64 Enterprise: ami-0fe045659a23cff48 -# ap-northeast-2 v17.0.4 x86_64 Enterprise: ami-04ce28140625e2dee -# ap-northeast-3 v17.0.4 arm64 Enterprise: ami-04c911f34da45d0b4 -# ap-northeast-3 v17.0.4 x86_64 Enterprise: ami-029ad395b820c9d13 -# ap-south-1 v17.0.4 arm64 Enterprise: ami-02e80813715c2edce -# ap-south-1 v17.0.4 x86_64 Enterprise: ami-0ff485dc38ba524ee -# ap-southeast-1 v17.0.4 arm64 Enterprise: ami-0ef089f8e3c8b2948 -# ap-southeast-1 v17.0.4 x86_64 Enterprise: ami-0304c1638e8896047 -# ap-southeast-2 v17.0.4 arm64 Enterprise: ami-051c847519cd353b5 -# ap-southeast-2 v17.0.4 x86_64 Enterprise: ami-01a4cb81d27479b71 -# ca-central-1 v17.0.4 arm64 Enterprise: ami-0e7ff49b010ef7510 -# ca-central-1 v17.0.4 x86_64 Enterprise: ami-070729ca8a3cd52d4 -# eu-central-1 v17.0.4 arm64 Enterprise: ami-065ac13475462188a -# eu-central-1 v17.0.4 x86_64 Enterprise: ami-09ba3a9085883f7c5 -# eu-north-1 v17.0.4 arm64 Enterprise: ami-0871621387aea29c0 -# eu-north-1 v17.0.4 x86_64 Enterprise: ami-018bd329b835264ee -# eu-west-1 v17.0.4 arm64 Enterprise: ami-0ad04ae06926ca8a6 -# eu-west-1 v17.0.4 x86_64 Enterprise: ami-0e291eeaac1af9b96 -# eu-west-2 v17.0.4 arm64 Enterprise: ami-0147ea5263de3ff82 -# eu-west-2 v17.0.4 x86_64 Enterprise: ami-0712aa8f4d45d7f63 -# eu-west-3 v17.0.4 arm64 Enterprise: ami-0836ab80362bb163a -# eu-west-3 v17.0.4 x86_64 Enterprise: ami-00e63b301d64501bd -# sa-east-1 v17.0.4 arm64 Enterprise: ami-05c4af22838178da8 -# sa-east-1 v17.0.4 x86_64 Enterprise: ami-0653dc7e72358fdf4 -# us-east-1 v17.0.4 arm64 Enterprise: ami-094bdb3b5c7462211 -# us-east-1 v17.0.4 x86_64 Enterprise: ami-06fa756239fba1686 -# us-east-2 v17.0.4 arm64 Enterprise: ami-0d0799739729e9282 -# us-east-2 v17.0.4 x86_64 Enterprise: ami-01a960de052a65ced -# us-west-1 v17.0.4 arm64 Enterprise: ami-0d8ea901cc1172268 -# us-west-1 v17.0.4 x86_64 Enterprise: ami-041d4d9e8258802a2 -# us-west-2 v17.0.4 arm64 Enterprise: ami-0917840ac9f9e0683 -# us-west-2 v17.0.4 x86_64 Enterprise: ami-0dfdb666a13565a1e +# ap-northeast-1 v17.1.0 arm64 Enterprise: ami-0054beb624b93a3d0 +# ap-northeast-1 v17.1.0 x86_64 Enterprise: ami-0dbef8379e4c2c225 +# ap-northeast-2 v17.1.0 arm64 Enterprise: ami-0c38f0f52cd32a5ed +# ap-northeast-2 v17.1.0 x86_64 Enterprise: ami-049e5fbf9f1d9fce8 +# ap-northeast-3 v17.1.0 arm64 Enterprise: ami-00656bf0363c42865 +# ap-northeast-3 v17.1.0 x86_64 Enterprise: ami-001609c2cb7449fbc +# ap-south-1 v17.1.0 arm64 Enterprise: ami-036ece94f64bf3ea7 +# ap-south-1 v17.1.0 x86_64 Enterprise: ami-0b42050e95f926aeb +# ap-southeast-1 v17.1.0 arm64 Enterprise: ami-06a26bb15bd9ca42a +# ap-southeast-1 v17.1.0 x86_64 Enterprise: ami-0c5cf93801310fc9c +# ap-southeast-2 v17.1.0 arm64 Enterprise: ami-0a3bc5db7482b39e8 +# ap-southeast-2 v17.1.0 x86_64 Enterprise: ami-063a7f0c4ce5b1db2 +# ca-central-1 v17.1.0 arm64 Enterprise: ami-080d8bd9ad1ab8fc7 +# ca-central-1 v17.1.0 x86_64 Enterprise: ami-00a89e0f143218b4f +# eu-central-1 v17.1.0 arm64 Enterprise: ami-0c24ed4801d5d8930 +# eu-central-1 v17.1.0 x86_64 Enterprise: ami-08e2317992e40cae6 +# eu-north-1 v17.1.0 arm64 Enterprise: ami-0272e7fe996421cb3 +# eu-north-1 v17.1.0 x86_64 Enterprise: ami-0f324a587f30af7a9 +# eu-west-1 v17.1.0 arm64 Enterprise: ami-0a016a18b345679c8 +# eu-west-1 v17.1.0 x86_64 Enterprise: ami-092241ead9afd9dfe +# eu-west-2 v17.1.0 arm64 Enterprise: ami-098ca4be33a079de5 +# eu-west-2 v17.1.0 x86_64 Enterprise: ami-05d03222fd3f32735 +# eu-west-3 v17.1.0 arm64 Enterprise: ami-05f4a7124ca6c2c66 +# eu-west-3 v17.1.0 x86_64 Enterprise: ami-0bf3fff32d23ef3c1 +# sa-east-1 v17.1.0 arm64 Enterprise: ami-0610008b54140b89b +# sa-east-1 v17.1.0 x86_64 Enterprise: ami-00da41d2bf0698b3f +# us-east-1 v17.1.0 arm64 Enterprise: ami-0bf51a258f80e2ec2 +# us-east-1 v17.1.0 x86_64 Enterprise: ami-0b1da60eb131922ab +# us-east-2 v17.1.0 arm64 Enterprise: ami-0a4367cf4cb92be24 +# us-east-2 v17.1.0 x86_64 Enterprise: ami-0c4bc58b0d9ef0c61 +# us-west-1 v17.1.0 arm64 Enterprise: ami-0660af60a0aba46c1 +# us-west-1 v17.1.0 x86_64 Enterprise: ami-05c3f83de44830858 +# us-west-2 v17.1.0 arm64 Enterprise: ami-0f2da6ca41d40371a +# us-west-2 v17.1.0 x86_64 Enterprise: ami-0797199b139ee8a16 ``` ### Enterprise FIPS ``` -# ap-northeast-1 v17.0.4 arm64 Enterprise FIPS: ami-09e383034b7defdaf -# ap-northeast-1 v17.0.4 x86_64 Enterprise FIPS: ami-0df69b8a4380e1de6 -# ap-northeast-2 v17.0.4 arm64 Enterprise FIPS: ami-0f8d28e7bcefe2970 -# ap-northeast-2 v17.0.4 x86_64 Enterprise FIPS: ami-0197da2adec541176 -# ap-northeast-3 v17.0.4 arm64 Enterprise FIPS: ami-06f2fab51b401aa96 -# ap-northeast-3 v17.0.4 x86_64 Enterprise FIPS: ami-0076f778514b9496d -# ap-south-1 v17.0.4 arm64 Enterprise FIPS: ami-07ea988d6c2412e73 -# ap-south-1 v17.0.4 x86_64 Enterprise FIPS: ami-0ac1183e7314ea34d -# ap-southeast-1 v17.0.4 arm64 Enterprise FIPS: ami-0f4a91f7e074c1771 -# ap-southeast-1 v17.0.4 x86_64 Enterprise FIPS: ami-01ee28a10a5e1e11f -# ap-southeast-2 v17.0.4 arm64 Enterprise FIPS: ami-0a65998ce9d562d79 -# ap-southeast-2 v17.0.4 x86_64 Enterprise FIPS: ami-0e1b23bcaaf49287c -# ca-central-1 v17.0.4 arm64 Enterprise FIPS: ami-05ecb968b73974203 -# ca-central-1 v17.0.4 x86_64 Enterprise FIPS: ami-07491a64d004d3291 -# eu-central-1 v17.0.4 arm64 Enterprise FIPS: ami-08a2592c33f8ea84e -# eu-central-1 v17.0.4 x86_64 Enterprise FIPS: ami-0f9e368192f2ec651 -# eu-north-1 v17.0.4 arm64 Enterprise FIPS: ami-0b380397bb540ce29 -# eu-north-1 v17.0.4 x86_64 Enterprise FIPS: ami-023badde3df29ae70 -# eu-west-1 v17.0.4 arm64 Enterprise FIPS: ami-03490ba6be4c23476 -# eu-west-1 v17.0.4 x86_64 Enterprise FIPS: ami-0d5f2c95321fe98f2 -# eu-west-2 v17.0.4 arm64 Enterprise FIPS: ami-03437b2758ba462c9 -# eu-west-2 v17.0.4 x86_64 Enterprise FIPS: ami-0f569292c54f0569f -# eu-west-3 v17.0.4 arm64 Enterprise FIPS: ami-02bf987212952aab2 -# eu-west-3 v17.0.4 x86_64 Enterprise FIPS: ami-0cbe8a49f10fa96ff -# sa-east-1 v17.0.4 arm64 Enterprise FIPS: ami-096f403881ccb12ec -# sa-east-1 v17.0.4 x86_64 Enterprise FIPS: ami-025711384880cb5a8 -# us-east-1 v17.0.4 arm64 Enterprise FIPS: ami-0c1506fe582a035bc -# us-east-1 v17.0.4 x86_64 Enterprise FIPS: ami-083791d36cad90c5b -# us-east-2 v17.0.4 arm64 Enterprise FIPS: ami-0beabe7f3c09fbe87 -# us-east-2 v17.0.4 x86_64 Enterprise FIPS: ami-02d7a281b2c843203 -# us-west-1 v17.0.4 arm64 Enterprise FIPS: ami-0e5a7de7a6163d9da -# us-west-1 v17.0.4 x86_64 Enterprise FIPS: ami-01298486181f8e6b0 -# us-west-2 v17.0.4 arm64 Enterprise FIPS: ami-0aff456ddb4a86a85 -# us-west-2 v17.0.4 x86_64 Enterprise FIPS: ami-0998561252ed04b71 +# ap-northeast-1 v17.1.0 arm64 Enterprise FIPS: ami-006e277130d6697b8 +# ap-northeast-1 v17.1.0 x86_64 Enterprise FIPS: ami-0d2097cc785d5ddd1 +# ap-northeast-2 v17.1.0 arm64 Enterprise FIPS: ami-05d7526f2fa772bae +# ap-northeast-2 v17.1.0 x86_64 Enterprise FIPS: ami-0b95d703d0fb35264 +# ap-northeast-3 v17.1.0 arm64 Enterprise FIPS: ami-058a1688e3746346f +# ap-northeast-3 v17.1.0 x86_64 Enterprise FIPS: ami-0a3609711beac9b12 +# ap-south-1 v17.1.0 arm64 Enterprise FIPS: ami-04fd535409f9546b2 +# ap-south-1 v17.1.0 x86_64 Enterprise FIPS: ami-0c8eb1f599b03ba3d +# ap-southeast-1 v17.1.0 arm64 Enterprise FIPS: ami-0859705827146286a +# ap-southeast-1 v17.1.0 x86_64 Enterprise FIPS: ami-08ce8518812498f8e +# ap-southeast-2 v17.1.0 arm64 Enterprise FIPS: ami-099243139967a52ef +# ap-southeast-2 v17.1.0 x86_64 Enterprise FIPS: ami-04e9d6967cf82c029 +# ca-central-1 v17.1.0 arm64 Enterprise FIPS: ami-0724d756ac222374c +# ca-central-1 v17.1.0 x86_64 Enterprise FIPS: ami-0ba9b501211abdfa3 +# eu-central-1 v17.1.0 arm64 Enterprise FIPS: ami-0db5328407fdf56cb +# eu-central-1 v17.1.0 x86_64 Enterprise FIPS: ami-0c811a9693f2ddb6e +# eu-north-1 v17.1.0 arm64 Enterprise FIPS: ami-0e70137a138a7c919 +# eu-north-1 v17.1.0 x86_64 Enterprise FIPS: ami-01ed85ddc0cdbafb4 +# eu-west-1 v17.1.0 arm64 Enterprise FIPS: ami-04678f3cda0b28ae8 +# eu-west-1 v17.1.0 x86_64 Enterprise FIPS: ami-0f2ccfb2586e70f99 +# eu-west-2 v17.1.0 arm64 Enterprise FIPS: ami-0e89768c72aa20b1a +# eu-west-2 v17.1.0 x86_64 Enterprise FIPS: ami-0fb819f8af7d2c478 +# eu-west-3 v17.1.0 arm64 Enterprise FIPS: ami-0e6824016a67d2446 +# eu-west-3 v17.1.0 x86_64 Enterprise FIPS: ami-010efc79993d1ea33 +# sa-east-1 v17.1.0 arm64 Enterprise FIPS: ami-0658ae24643ca38e6 +# sa-east-1 v17.1.0 x86_64 Enterprise FIPS: ami-0d14d90a5352b3771 +# us-east-1 v17.1.0 arm64 Enterprise FIPS: ami-08d29a6b64f3344be +# us-east-1 v17.1.0 x86_64 Enterprise FIPS: ami-0430151efc1689906 +# us-east-2 v17.1.0 arm64 Enterprise FIPS: ami-0da38d632101bf3d1 +# us-east-2 v17.1.0 x86_64 Enterprise FIPS: ami-09340caeb2a02f47d +# us-west-1 v17.1.0 arm64 Enterprise FIPS: ami-0456b2b106888013e +# us-west-1 v17.1.0 x86_64 Enterprise FIPS: ami-0b58799c4f10967c2 +# us-west-2 v17.1.0 arm64 Enterprise FIPS: ami-06b404ae4fed6d673 +# us-west-2 v17.1.0 x86_64 Enterprise FIPS: ami-084c64cf7ead63339 ``` diff --git a/examples/aws/terraform/ha-autoscale-cluster/README.md b/examples/aws/terraform/ha-autoscale-cluster/README.md index 808929be0b49c..3aea34e392677 100644 --- a/examples/aws/terraform/ha-autoscale-cluster/README.md +++ b/examples/aws/terraform/ha-autoscale-cluster/README.md @@ -46,7 +46,7 @@ export TF_VAR_cluster_name="teleport.example.com" # OSS: aws ec2 describe-images --owners 146628656107 --filters 'Name=name,Values=teleport-oss-*' # Enterprise: aws ec2 describe-images --owners 146628656107 --filters 'Name=name,Values=teleport-ent-*' # FIPS 140-2 images are also available for Enterprise customers, look for '-fips' on the end of the AMI's name -export TF_VAR_ami_name="teleport-ent-17.0.4-arm64" +export TF_VAR_ami_name="teleport-ent-17.1.0-arm64" # Instance types used for authentication server auto scaling group # This should match to the AMI instance architecture type, ARM or x86 diff --git a/examples/aws/terraform/starter-cluster/README.md b/examples/aws/terraform/starter-cluster/README.md index 0fa8984f8c78b..be6ef3107f3f3 100644 --- a/examples/aws/terraform/starter-cluster/README.md +++ b/examples/aws/terraform/starter-cluster/README.md @@ -98,7 +98,7 @@ TF_VAR_license_path ?= "/path/to/license" # OSS: aws ec2 describe-images --owners 146628656107 --filters 'Name=name,Values=teleport-oss-*' # Enterprise: aws ec2 describe-images --owners 146628656107 --filters 'Name=name,Values=teleport-ent-*' # FIPS 140-2 images are also available for Enterprise customers, look for '-fips' on the end of the AMI's name -TF_VAR_ami_name ?= "teleport-ent-17.0.4-arm64" +TF_VAR_ami_name ?= "teleport-ent-17.1.0-arm64" # Route 53 hosted zone to use, must be a root zone registered in AWS, e.g. example.com TF_VAR_route53_zone ?= "example.com" From 7269273fb5b9a9e1d6cf30da63abfc44a5678db9 Mon Sep 17 00:00:00 2001 From: Brian Joerger Date: Thu, 19 Dec 2024 19:51:12 -0800 Subject: [PATCH 31/64] Add SSO MFA prompt for WebUI MFA flows (#49794) * Include sso channel ID in web mfa challenges. * Handle SSO MFA challenges. * Handle sso response in backend. * Handle non-webauthn mfa response for file transfer, admin actions, and app session. * Simplify useMfa with new helpers. * Fix lint. * Use AuthnDialog for file transfers; Fix json backend logic for file transfers. * Make useMfa and AuthnDialog more reusable and error proof. * Use AuthnDialog for App sessions. * Resolve comments. * Fix broken app launcher; improve mfaRequired logic in useMfa. * Fix AuthnDialog test. * Fix merge conflict with Db web access. * fix stories. * Refactor mfa required logic. * Address bl-nero's comments. * Address Ryan's comments. * Add useMfa unit test. * Fix story lint. * Replace Promise.withResolvers for compatiblity with older browers; Fix bug where MFA couldn't be retried after a failed attempt; Add extra tests. --- lib/client/weblogin.go | 46 ++- lib/web/apiserver.go | 12 +- lib/web/apiserver_test.go | 8 +- lib/web/apps.go | 29 +- lib/web/files.go | 47 ++- lib/web/mfa.go | 27 +- lib/web/mfajson/mfajson.go | 40 +- lib/web/password.go | 2 +- .../wizards/AddAuthDeviceWizard.test.tsx | 60 ++- .../wizards/DeleteAuthDeviceWizard.test.tsx | 46 ++- .../src/AppLauncher/AppLauncher.test.tsx | 4 +- .../teleport/src/AppLauncher/AppLauncher.tsx | 38 +- .../src/Console/DocumentDb/DocumentDb.tsx | 16 +- .../DocumentKubeExec/DocumentKubeExec.tsx | 8 +- .../src/Console/DocumentSsh/DocumentSsh.tsx | 51 +-- .../Console/DocumentSsh/useFileTransfer.ts | 30 +- .../src/Console/DocumentSsh/useGetScpUrl.ts | 66 ---- .../DesktopSession/DesktopSession.story.tsx | 23 +- .../src/DesktopSession/DesktopSession.tsx | 12 +- .../src/DesktopSession/useDesktopSession.tsx | 4 +- .../AuthnDialog/AuthnDialog.story.tsx | 70 ++-- .../AuthnDialog/AuthnDialog.test.tsx | 74 +++- .../components/AuthnDialog/AuthnDialog.tsx | 115 +++--- .../ReAuthenticate/useReAuthenticate.ts | 9 +- web/packages/teleport/src/config.ts | 27 +- .../teleport/src/lib/EventEmitterMfaSender.ts | 14 +- web/packages/teleport/src/lib/tdp/client.ts | 9 - web/packages/teleport/src/lib/term/tty.ts | 13 +- web/packages/teleport/src/lib/useMfa.test.tsx | 246 ++++++++++++ web/packages/teleport/src/lib/useMfa.ts | 360 +++++++++--------- .../teleport/src/services/api/api.test.ts | 6 +- web/packages/teleport/src/services/api/api.ts | 8 +- .../teleport/src/services/apps/apps.ts | 39 +- .../teleport/src/services/auth/auth.ts | 76 ++++ .../teleport/src/services/mfa/mfaOptions.ts | 4 +- .../teleport/src/services/mfa/types.ts | 6 +- 36 files changed, 1043 insertions(+), 602 deletions(-) delete mode 100644 web/packages/teleport/src/Console/DocumentSsh/useGetScpUrl.ts create mode 100644 web/packages/teleport/src/lib/useMfa.test.tsx diff --git a/lib/client/weblogin.go b/lib/client/weblogin.go index 7edf946c0e39f..c3415e340417d 100644 --- a/lib/client/weblogin.go +++ b/lib/client/weblogin.go @@ -113,6 +113,8 @@ type MFAChallengeResponse struct { WebauthnResponse *wantypes.CredentialAssertionResponse `json:"webauthn_response,omitempty"` // SSOResponse is a response from an SSO MFA flow. SSOResponse *SSOResponse `json:"sso_response"` + // TODO(Joerger): DELETE IN v19.0.0, WebauthnResponse used instead. + WebauthnAssertionResponse *wantypes.CredentialAssertionResponse `json:"webauthnAssertionResponse"` } // SSOResponse is a json compatible [proto.SSOResponse]. @@ -124,25 +126,57 @@ type SSOResponse struct { // GetOptionalMFAResponseProtoReq converts response to a type proto.MFAAuthenticateResponse, // if there were any responses set. Otherwise returns nil. func (r *MFAChallengeResponse) GetOptionalMFAResponseProtoReq() (*proto.MFAAuthenticateResponse, error) { - if r.TOTPCode != "" && r.WebauthnResponse != nil { + var availableResponses int + if r.TOTPCode != "" { + availableResponses++ + } + if r.WebauthnResponse != nil { + availableResponses++ + } + if r.SSOResponse != nil { + availableResponses++ + } + + if availableResponses > 1 { return nil, trace.BadParameter("only one MFA response field can be set") } - if r.TOTPCode != "" { + switch { + case r.WebauthnResponse != nil: + return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_Webauthn{ + Webauthn: wantypes.CredentialAssertionResponseToProto(r.WebauthnResponse), + }}, nil + case r.SSOResponse != nil: + return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_SSO{ + SSO: &proto.SSOResponse{ + RequestId: r.SSOResponse.RequestID, + Token: r.SSOResponse.Token, + }, + }}, nil + case r.TOTPCode != "": return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_TOTP{ TOTP: &proto.TOTPResponse{Code: r.TOTPCode}, }}, nil - } - - if r.WebauthnResponse != nil { + case r.WebauthnAssertionResponse != nil: return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_Webauthn{ - Webauthn: wantypes.CredentialAssertionResponseToProto(r.WebauthnResponse), + Webauthn: wantypes.CredentialAssertionResponseToProto(r.WebauthnAssertionResponse), }}, nil } return nil, nil } +// ParseMFAChallengeResponse parses [MFAChallengeResponse] from JSON and returns it as a [proto.MFAAuthenticateResponse]. +func ParseMFAChallengeResponse(mfaResponseJSON []byte) (*proto.MFAAuthenticateResponse, error) { + var resp MFAChallengeResponse + if err := json.Unmarshal(mfaResponseJSON, &resp); err != nil { + return nil, trace.Wrap(err) + } + + protoResp, err := resp.GetOptionalMFAResponseProtoReq() + return protoResp, trace.Wrap(err) +} + // CreateSSHCertReq is passed by tsh to authenticate a local user without MFA // and receive short-lived certificates. type CreateSSHCertReq struct { diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index a81932f586de4..451f5668a14a2 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -2766,7 +2766,7 @@ func (h *Handler) mfaLoginBegin(w http.ResponseWriter, r *http.Request, p httpro return nil, trace.AccessDenied("invalid credentials") } - return makeAuthenticateChallenge(mfaChallenge), nil + return makeAuthenticateChallenge(mfaChallenge, "" /*channelID*/), nil } // mfaLoginFinish completes the MFA login ceremony, returning a new SSH @@ -4857,16 +4857,12 @@ func parseMFAResponseFromRequest(r *http.Request) error { // context and returned. func contextWithMFAResponseFromRequestHeader(ctx context.Context, requestHeader http.Header) (context.Context, error) { if mfaResponseJSON := requestHeader.Get("Teleport-MFA-Response"); mfaResponseJSON != "" { - var resp mfaResponse - if err := json.Unmarshal([]byte(mfaResponseJSON), &resp); err != nil { + mfaResp, err := client.ParseMFAChallengeResponse([]byte(mfaResponseJSON)) + if err != nil { return nil, trace.Wrap(err) } - return mfa.ContextWithMFAResponse(ctx, &proto.MFAAuthenticateResponse{ - Response: &proto.MFAAuthenticateResponse_Webauthn{ - Webauthn: wantypes.CredentialAssertionResponseToProto(resp.WebauthnAssertionResponse), - }, - }), nil + return mfa.ContextWithMFAResponse(ctx, mfaResp), nil } return ctx, nil diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index f3a71ebb78945..2816f2f92b7bb 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -5573,10 +5573,6 @@ func TestCreateAppSession_RequireSessionMFA(t *testing.T) { require.NoError(t, err) mfaResp, err := webauthnDev.SolveAuthn(chal) require.NoError(t, err) - mfaRespJSON, err := json.Marshal(mfaResponse{ - WebauthnAssertionResponse: wantypes.CredentialAssertionResponseFromProto(mfaResp.GetWebauthn()), - }) - require.NoError(t, err) // Extract the session ID and bearer token for the current session. rawCookie := *pack.cookies[0] @@ -5610,7 +5606,9 @@ func TestCreateAppSession_RequireSessionMFA(t *testing.T) { PublicAddr: "panel.example.com", ClusterName: "localhost", }, - MFAResponse: string(mfaRespJSON), + MFAResponse: client.MFAChallengeResponse{ + WebauthnAssertionResponse: wantypes.CredentialAssertionResponseFromProto(mfaResp.GetWebauthn()), + }, }, expectMFAVerified: true, }, diff --git a/lib/web/apps.go b/lib/web/apps.go index 8ae0dc5525468..0facc0436d03a 100644 --- a/lib/web/apps.go +++ b/lib/web/apps.go @@ -22,7 +22,6 @@ package web import ( "context" - "encoding/json" "net/http" "sort" @@ -33,7 +32,7 @@ import ( "github.com/gravitational/teleport/api/client/proto" apidefaults "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" - wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" + "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/httplib" "github.com/gravitational/teleport/lib/reversetunnelclient" "github.com/gravitational/teleport/lib/utils" @@ -191,7 +190,10 @@ type CreateAppSessionRequest struct { // AWSRole is the AWS role ARN when accessing AWS management console. AWSRole string `json:"arn,omitempty"` // MFAResponse is an optional MFA response used to create an MFA verified app session. - MFAResponse string `json:"mfa_response"` + MFAResponse client.MFAChallengeResponse `json:"mfaResponse"` + // TODO(Joerger): DELETE IN v19.0.0 + // Backwards compatible version of MFAResponse + MFAResponseJSON string `json:"mfa_response"` } // CreateAppSessionResponse is a response to POST /v1/webapi/sessions/app @@ -230,17 +232,16 @@ func (h *Handler) createAppSession(w http.ResponseWriter, r *http.Request, p htt } } - var mfaProtoResponse *proto.MFAAuthenticateResponse - if req.MFAResponse != "" { - var resp mfaResponse - if err := json.Unmarshal([]byte(req.MFAResponse), &resp); err != nil { - return nil, trace.Wrap(err) - } + mfaResponse, err := req.MFAResponse.GetOptionalMFAResponseProtoReq() + if err != nil { + return nil, trace.Wrap(err) + } - mfaProtoResponse = &proto.MFAAuthenticateResponse{ - Response: &proto.MFAAuthenticateResponse_Webauthn{ - Webauthn: wantypes.CredentialAssertionResponseToProto(resp.WebauthnAssertionResponse), - }, + // Fallback to backwards compatible mfa response. + if mfaResponse == nil && req.MFAResponseJSON != "" { + mfaResponse, err = client.ParseMFAChallengeResponse([]byte(req.MFAResponseJSON)) + if err != nil { + return nil, trace.Wrap(err) } } @@ -263,7 +264,7 @@ func (h *Handler) createAppSession(w http.ResponseWriter, r *http.Request, p htt PublicAddr: result.App.GetPublicAddr(), ClusterName: result.ClusterName, AWSRoleARN: req.AWSRole, - MFAResponse: mfaProtoResponse, + MFAResponse: mfaResponse, AppName: result.App.GetName(), URI: result.App.GetURI(), ClientAddr: r.RemoteAddr, diff --git a/lib/web/files.go b/lib/web/files.go index 53248258dd034..1c48dbf4f745e 100644 --- a/lib/web/files.go +++ b/lib/web/files.go @@ -20,7 +20,6 @@ package web import ( "context" - "encoding/json" "errors" "net/http" "time" @@ -35,7 +34,6 @@ import ( "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/api/utils/sshutils" "github.com/gravitational/teleport/lib/auth/authclient" - wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/multiplexer" "github.com/gravitational/teleport/lib/reversetunnelclient" @@ -56,8 +54,8 @@ type fileTransferRequest struct { remoteLocation string // filename is a file name filename string - // webauthn is an optional parameter that contains a webauthn response string used to issue single use certs - webauthn string + // mfaResponse is an optional parameter that contains an mfa response string used to issue single use certs + mfaResponse string // fileTransferRequestID is used to find a FileTransferRequest on a session fileTransferRequestID string // moderatedSessonID is an ID of a moderated session that has completed a @@ -74,11 +72,25 @@ func (h *Handler) transferFile(w http.ResponseWriter, r *http.Request, p httprou remoteLocation: query.Get("location"), filename: query.Get("filename"), namespace: defaults.Namespace, - webauthn: query.Get("webauthn"), + mfaResponse: query.Get("mfaResponse"), fileTransferRequestID: query.Get("fileTransferRequestId"), moderatedSessionID: query.Get("moderatedSessionId"), } + // Check for old query parameter, uses the same data structure. + // TODO(Joerger): DELETE IN v19.0.0 + if req.mfaResponse == "" { + req.mfaResponse = query.Get("webauthn") + } + + var mfaResponse *proto.MFAAuthenticateResponse + if req.mfaResponse != "" { + var err error + if mfaResponse, err = client.ParseMFAChallengeResponse([]byte(req.mfaResponse)); err != nil { + return nil, trace.Wrap(err) + } + } + // Send an error if only one of these params has been sent. Both should exist or not exist together if (req.fileTransferRequestID != "") != (req.moderatedSessionID != "") { return nil, trace.BadParameter("fileTransferRequestId and moderatedSessionId must both be included in the same request.") @@ -107,7 +119,7 @@ func (h *Handler) transferFile(w http.ResponseWriter, r *http.Request, p httprou return nil, trace.Wrap(err) } - if mfaReq.Required && query.Get("webauthn") == "" { + if mfaReq.Required && mfaResponse == nil { return nil, trace.AccessDenied("MFA required for file transfer") } @@ -135,8 +147,8 @@ func (h *Handler) transferFile(w http.ResponseWriter, r *http.Request, p httprou return nil, trace.Wrap(err) } - if req.webauthn != "" { - err = ft.issueSingleUseCert(req.webauthn, r, tc) + if req.mfaResponse != "" { + err = ft.issueSingleUseCert(mfaResponse, r, tc) if err != nil { return nil, trace.Wrap(err) } @@ -216,21 +228,10 @@ func (f *fileTransfer) createClient(req fileTransferRequest, httpReq *http.Reque return tc, nil } -type mfaResponse struct { - // WebauthnResponse is the response from authenticators. - WebauthnAssertionResponse *wantypes.CredentialAssertionResponse `json:"webauthnAssertionResponse"` -} - // issueSingleUseCert will take an assertion response sent from a solved challenge in the web UI // and use that to generate a cert. This cert is added to the Teleport Client as an authmethod that // can be used to connect to a node. -func (f *fileTransfer) issueSingleUseCert(webauthn string, httpReq *http.Request, tc *client.TeleportClient) error { - var mfaResp mfaResponse - err := json.Unmarshal([]byte(webauthn), &mfaResp) - if err != nil { - return trace.Wrap(err) - } - +func (f *fileTransfer) issueSingleUseCert(mfaResponse *proto.MFAAuthenticateResponse, httpReq *http.Request, tc *client.TeleportClient) error { pk, err := keys.ParsePrivateKey(f.sctx.cfg.Session.GetSSHPriv()) if err != nil { return trace.Wrap(err) @@ -241,11 +242,7 @@ func (f *fileTransfer) issueSingleUseCert(webauthn string, httpReq *http.Request SSHPublicKey: pk.MarshalSSHPublicKey(), Username: f.sctx.GetUser(), Expires: time.Now().Add(time.Minute).UTC(), - MFAResponse: &proto.MFAAuthenticateResponse{ - Response: &proto.MFAAuthenticateResponse_Webauthn{ - Webauthn: wantypes.CredentialAssertionResponseToProto(mfaResp.WebauthnAssertionResponse), - }, - }, + MFAResponse: mfaResponse, }) if err != nil { return trace.Wrap(err) diff --git a/lib/web/mfa.go b/lib/web/mfa.go index 485a4eff460bc..c59b0ae10cbd7 100644 --- a/lib/web/mfa.go +++ b/lib/web/mfa.go @@ -21,8 +21,10 @@ package web import ( "context" "net/http" + "net/url" "strings" + "github.com/google/uuid" "github.com/gravitational/trace" "github.com/julienschmidt/httprouter" @@ -201,6 +203,22 @@ func (h *Handler) createAuthenticateChallengeHandle(w http.ResponseWriter, r *ht allowReuse = mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES } + // Prepare an sso client redirect URL in case the user has an SSO MFA device. + ssoClientRedirectURL, err := url.Parse(sso.WebMFARedirect) + if err != nil { + return nil, trace.Wrap(err) + } + + // id is used by the front end to differentiate between separate ongoing SSO challenges. + id, err := uuid.NewRandom() + if err != nil { + return nil, trace.Wrap(err) + } + channelID := id.String() + query := ssoClientRedirectURL.Query() + query.Set("channel_id", channelID) + ssoClientRedirectURL.RawQuery = query.Encode() + chal, err := clt.CreateAuthenticateChallenge(r.Context(), &proto.CreateAuthenticateChallengeRequest{ Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{ ContextUser: &proto.ContextUser{}, @@ -211,13 +229,13 @@ func (h *Handler) createAuthenticateChallengeHandle(w http.ResponseWriter, r *ht AllowReuse: allowReuse, UserVerificationRequirement: req.UserVerificationRequirement, }, - SSOClientRedirectURL: sso.WebMFARedirect, + SSOClientRedirectURL: ssoClientRedirectURL.String(), }) if err != nil { return nil, trace.Wrap(err) } - return makeAuthenticateChallenge(chal), nil + return makeAuthenticateChallenge(chal, channelID), nil } // createAuthenticateChallengeWithTokenHandle creates and returns MFA authenticate challenges for the user defined in token. @@ -235,7 +253,7 @@ func (h *Handler) createAuthenticateChallengeWithTokenHandle(w http.ResponseWrit return nil, trace.Wrap(err) } - return makeAuthenticateChallenge(chal), nil + return makeAuthenticateChallenge(chal, "" /*channelID*/), nil } type createRegisterChallengeWithTokenRequest struct { @@ -581,7 +599,7 @@ func (h *Handler) checkMFARequired(ctx context.Context, req *isMFARequiredReques } // makeAuthenticateChallenge converts proto to JSON format. -func makeAuthenticateChallenge(protoChal *proto.MFAAuthenticateChallenge) *client.MFAAuthenticateChallenge { +func makeAuthenticateChallenge(protoChal *proto.MFAAuthenticateChallenge, ssoChannelID string) *client.MFAAuthenticateChallenge { chal := &client.MFAAuthenticateChallenge{ TOTPChallenge: protoChal.GetTOTP() != nil, } @@ -590,6 +608,7 @@ func makeAuthenticateChallenge(protoChal *proto.MFAAuthenticateChallenge) *clien } if protoChal.GetSSOChallenge() != nil { chal.SSOChallenge = client.SSOChallengeFromProto(protoChal.GetSSOChallenge()) + chal.SSOChallenge.ChannelID = ssoChannelID } return chal } diff --git a/lib/web/mfajson/mfajson.go b/lib/web/mfajson/mfajson.go index 70abb8ecfec32..2105b0178b3a9 100644 --- a/lib/web/mfajson/mfajson.go +++ b/lib/web/mfajson/mfajson.go @@ -28,7 +28,7 @@ import ( "github.com/gravitational/teleport/lib/client" ) -// TODO(Joerger): DELETE IN v18.0.0 and use client.MFAChallengeResponse instead. +// TODO(Joerger): DELETE IN v19.0.0 and use client.MFAChallengeResponse instead. // Before v17, the WebUI sends a flattened webauthn response instead of a full // MFA challenge response. Newer WebUI versions v17+ will send both for // backwards compatibility. @@ -45,33 +45,17 @@ func Decode(b []byte, typ string) (*authproto.MFAAuthenticateResponse, error) { return nil, trace.Wrap(err) } - switch { - case resp.CredentialAssertionResponse != nil: - return &authproto.MFAAuthenticateResponse{ - Response: &authproto.MFAAuthenticateResponse_Webauthn{ - Webauthn: wantypes.CredentialAssertionResponseToProto(resp.CredentialAssertionResponse), - }, - }, nil - case resp.WebauthnResponse != nil: - return &authproto.MFAAuthenticateResponse{ - Response: &authproto.MFAAuthenticateResponse_Webauthn{ - Webauthn: wantypes.CredentialAssertionResponseToProto(resp.WebauthnResponse), - }, - }, nil - case resp.SSOResponse != nil: - return &authproto.MFAAuthenticateResponse{ - Response: &authproto.MFAAuthenticateResponse_SSO{ - SSO: &authproto.SSOResponse{ - RequestId: resp.SSOResponse.RequestID, - Token: resp.SSOResponse.Token, - }, - }, - }, nil - case resp.TOTPCode != "": - // Note: we can support TOTP through the websocket if desired, we just need to add - // a TOTP prompt modal and flip the switch here. - return nil, trace.BadParameter("totp is not supported in the WebUI") - default: + // Move flattened webauthn response into resp. + resp.MFAChallengeResponse.WebauthnAssertionResponse = resp.CredentialAssertionResponse + + protoResp, err := resp.GetOptionalMFAResponseProtoReq() + if err != nil { + return nil, trace.Wrap(err) + } + + if protoResp == nil { return nil, trace.BadParameter("invalid MFA response from web") } + + return protoResp, trace.Wrap(err) } diff --git a/lib/web/password.go b/lib/web/password.go index 6ae5923787d7e..824c8b00ecb5a 100644 --- a/lib/web/password.go +++ b/lib/web/password.go @@ -108,5 +108,5 @@ func (h *Handler) createAuthenticateChallengeWithPassword(w http.ResponseWriter, return nil, trace.Wrap(err) } - return makeAuthenticateChallenge(chal), nil + return makeAuthenticateChallenge(chal, "" /*channelID*/), nil } diff --git a/web/packages/teleport/src/Account/ManageDevices/wizards/AddAuthDeviceWizard.test.tsx b/web/packages/teleport/src/Account/ManageDevices/wizards/AddAuthDeviceWizard.test.tsx index 96a2fc05a404f..b4fb5a0303fe2 100644 --- a/web/packages/teleport/src/Account/ManageDevices/wizards/AddAuthDeviceWizard.test.tsx +++ b/web/packages/teleport/src/Account/ManageDevices/wizards/AddAuthDeviceWizard.test.tsx @@ -23,7 +23,7 @@ import { userEvent, UserEvent } from '@testing-library/user-event'; import { ContextProvider } from 'teleport'; import auth from 'teleport/services/auth'; -import MfaService from 'teleport/services/mfa'; +import MfaService, { SsoChallenge } from 'teleport/services/mfa'; import TeleportContext from 'teleport/teleportContext'; import { AddAuthDeviceWizardStepProps } from './AddAuthDeviceWizard'; @@ -170,11 +170,16 @@ describe('flow without reauthentication', () => { }); describe('flow with reauthentication', () => { + const dummyMfaChallenge = { + totpChallenge: true, + webauthnPublicKey: {} as PublicKeyCredentialRequestOptions, + ssoChallenge: {} as SsoChallenge, + }; + beforeEach(() => { - jest.spyOn(auth, 'getMfaChallenge').mockResolvedValueOnce({ - totpChallenge: true, - webauthnPublicKey: {} as PublicKeyCredentialRequestOptions, - }); + jest + .spyOn(auth, 'getMfaChallenge') + .mockResolvedValueOnce(dummyMfaChallenge); jest.spyOn(auth, 'getMfaChallengeResponse').mockResolvedValueOnce({}); jest .spyOn(auth, 'createPrivilegeToken') @@ -194,6 +199,11 @@ describe('flow with reauthentication', () => { expect(screen.getByTestId('create-step')).toBeInTheDocument(); }); await user.click(screen.getByRole('button', { name: 'Create a passkey' })); + expect(auth.getMfaChallengeResponse).toHaveBeenCalledWith( + dummyMfaChallenge, + 'webauthn', + '' + ); expect(auth.createNewWebAuthnDevice).toHaveBeenCalledWith({ tokenId: 'privilege-token', deviceUsage: 'passwordless', @@ -228,6 +238,46 @@ describe('flow with reauthentication', () => { expect(screen.getByTestId('create-step')).toBeInTheDocument(); }); await user.click(screen.getByRole('button', { name: 'Create a passkey' })); + expect(auth.getMfaChallengeResponse).toHaveBeenCalledWith( + dummyMfaChallenge, + 'totp', + '654987' + ); + expect(auth.createNewWebAuthnDevice).toHaveBeenCalledWith({ + tokenId: 'privilege-token', + deviceUsage: 'passwordless', + }); + + expect(screen.getByTestId('save-step')).toBeInTheDocument(); + await user.type(screen.getByLabelText('Passkey Nickname'), 'new-passkey'); + await user.click(screen.getByRole('button', { name: 'Save the Passkey' })); + expect(ctx.mfaService.saveNewWebAuthnDevice).toHaveBeenCalledWith({ + credential: dummyCredential, + addRequest: { + deviceName: 'new-passkey', + deviceUsage: 'passwordless', + tokenId: 'privilege-token', + }, + }); + expect(onSuccess).toHaveBeenCalled(); + }); + + test('adds a passkey with SSO reauthentication', async () => { + render(); + + await waitFor(() => { + expect(screen.getByTestId('reauthenticate-step')).toBeInTheDocument(); + }); + await user.click(screen.getByText('SSO')); + await user.click(screen.getByText('Verify my identity')); + + expect(screen.getByTestId('create-step')).toBeInTheDocument(); + await user.click(screen.getByRole('button', { name: 'Create a passkey' })); + expect(auth.getMfaChallengeResponse).toHaveBeenCalledWith( + dummyMfaChallenge, + 'sso', + '' + ); expect(auth.createNewWebAuthnDevice).toHaveBeenCalledWith({ tokenId: 'privilege-token', deviceUsage: 'passwordless', diff --git a/web/packages/teleport/src/Account/ManageDevices/wizards/DeleteAuthDeviceWizard.test.tsx b/web/packages/teleport/src/Account/ManageDevices/wizards/DeleteAuthDeviceWizard.test.tsx index dd780c4f3996f..c4e77e1365df7 100644 --- a/web/packages/teleport/src/Account/ManageDevices/wizards/DeleteAuthDeviceWizard.test.tsx +++ b/web/packages/teleport/src/Account/ManageDevices/wizards/DeleteAuthDeviceWizard.test.tsx @@ -23,7 +23,7 @@ import { userEvent, UserEvent } from '@testing-library/user-event'; import TeleportContext from 'teleport/teleportContext'; import { ContextProvider } from 'teleport'; -import MfaService from 'teleport/services/mfa'; +import MfaService, { SsoChallenge } from 'teleport/services/mfa'; import auth from 'teleport/services/auth'; import { DeleteAuthDeviceWizardStepProps } from './DeleteAuthDeviceWizard'; @@ -36,15 +36,18 @@ let ctx: TeleportContext; let user: UserEvent; let onSuccess: jest.Mock; +const dummyMfaChallenge = { + totpChallenge: true, + webauthnPublicKey: {} as PublicKeyCredentialRequestOptions, + ssoChallenge: {} as SsoChallenge, +}; + beforeEach(() => { ctx = new TeleportContext(); user = userEvent.setup(); onSuccess = jest.fn(); - jest.spyOn(auth, 'getMfaChallenge').mockResolvedValueOnce({ - totpChallenge: true, - webauthnPublicKey: {} as PublicKeyCredentialRequestOptions, - }); + jest.spyOn(auth, 'getMfaChallenge').mockResolvedValueOnce(dummyMfaChallenge); jest.spyOn(auth, 'getMfaChallengeResponse').mockResolvedValueOnce({}); jest .spyOn(auth, 'createPrivilegeToken') @@ -80,6 +83,11 @@ test('deletes a device with WebAuthn reauthentication', async () => { expect(screen.getByTestId('delete-step')).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: 'Delete' })); + expect(auth.getMfaChallengeResponse).toHaveBeenCalledWith( + dummyMfaChallenge, + 'webauthn', + '' + ); expect(ctx.mfaService.removeDevice).toHaveBeenCalledWith( 'privilege-token', 'TouchID' @@ -100,6 +108,34 @@ test('deletes a device with OTP reauthentication', async () => { expect(screen.getByTestId('delete-step')).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: 'Delete' })); + expect(auth.getMfaChallengeResponse).toHaveBeenCalledWith( + dummyMfaChallenge, + 'totp', + '654987' + ); + expect(ctx.mfaService.removeDevice).toHaveBeenCalledWith( + 'privilege-token', + 'TouchID' + ); +}); + +test('deletes a device with SSO reauthentication', async () => { + render(); + + await waitFor(() => { + expect(screen.getByTestId('reauthenticate-step')).toBeInTheDocument(); + }); + await user.click(screen.getByText('SSO')); + await user.click(screen.getByText('Verify my identity')); + + expect(screen.getByTestId('delete-step')).toBeInTheDocument(); + await user.click(screen.getByRole('button', { name: 'Delete' })); + + expect(auth.getMfaChallengeResponse).toHaveBeenCalledWith( + dummyMfaChallenge, + 'sso', + '' + ); expect(ctx.mfaService.removeDevice).toHaveBeenCalledWith( 'privilege-token', 'TouchID' diff --git a/web/packages/teleport/src/AppLauncher/AppLauncher.test.tsx b/web/packages/teleport/src/AppLauncher/AppLauncher.test.tsx index ae561d4950532..6b9e7fdf3400b 100644 --- a/web/packages/teleport/src/AppLauncher/AppLauncher.test.tsx +++ b/web/packages/teleport/src/AppLauncher/AppLauncher.test.tsx @@ -16,13 +16,13 @@ * along with this program. If not, see . */ -import { render, waitFor, screen } from 'design/utils/testing'; +import { render, screen, waitFor } from 'design/utils/testing'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; import { Route } from 'teleport/components/Router'; -import api from 'teleport/services/api'; import cfg from 'teleport/config'; +import api from 'teleport/services/api'; import service from 'teleport/services/apps'; import { AppLauncher } from './AppLauncher'; diff --git a/web/packages/teleport/src/AppLauncher/AppLauncher.tsx b/web/packages/teleport/src/AppLauncher/AppLauncher.tsx index 97d3559bb6365..78db3d6733f2d 100644 --- a/web/packages/teleport/src/AppLauncher/AppLauncher.tsx +++ b/web/packages/teleport/src/AppLauncher/AppLauncher.tsx @@ -26,8 +26,11 @@ import { AccessDenied } from 'design/CardError'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { UrlLauncherParams } from 'teleport/config'; +import AuthnDialog from 'teleport/components/AuthnDialog'; +import { CreateAppSessionParams, UrlLauncherParams } from 'teleport/config'; +import { useMfa } from 'teleport/lib/useMfa'; import service from 'teleport/services/apps'; +import { MfaChallengeScope } from 'teleport/services/auth/auth'; export function AppLauncher() { const { attempt, setAttempt } = useAttempt('processing'); @@ -37,6 +40,19 @@ export function AppLauncher() { const queryParams = new URLSearchParams(search); const isRedirectFlow = queryParams.get('required-apps'); + const mfa = useMfa({ + req: { + scope: MfaChallengeScope.USER_SESSION, + isMfaRequiredRequest: { + app: { + fqdn: pathParams.fqdn, + cluster_name: pathParams.clusterId, + public_addr: pathParams.publicAddr, + }, + }, + }, + }); + const createAppSession = useCallback(async (params: UrlLauncherParams) => { let fqdn = params.fqdn; const port = location.port ? `:${location.port}` : ''; @@ -101,7 +117,10 @@ export function AppLauncher() { if (params.arn) { params.arn = decodeURIComponent(params.arn); } - const session = await service.createAppSession(params); + + const createAppSessionParams = params as CreateAppSessionParams; + createAppSessionParams.mfaResponse = await mfa.getChallengeResponse(); + const session = await service.createAppSession(createAppSessionParams); // Set all the fields expected by server to validate request. const url = getXTeleportAuthUrl({ fqdn, port }); @@ -142,11 +161,16 @@ export function AppLauncher() { createAppSession(pathParams); }, [pathParams]); - if (attempt.status === 'failed') { - return ; - } - - return ; + return ( +
    + {attempt.status === 'failed' ? ( + + ) : ( + + )} + +
    + ); } export function AppLauncherProcessing() { diff --git a/web/packages/teleport/src/Console/DocumentDb/DocumentDb.tsx b/web/packages/teleport/src/Console/DocumentDb/DocumentDb.tsx index 0d6d333141b2a..6c024edfe7331 100644 --- a/web/packages/teleport/src/Console/DocumentDb/DocumentDb.tsx +++ b/web/packages/teleport/src/Console/DocumentDb/DocumentDb.tsx @@ -15,20 +15,20 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import { useRef, useEffect } from 'react'; +import { useEffect, useRef } from 'react'; -import { useTheme } from 'styled-components'; import { Box, Indicator } from 'design'; +import { useTheme } from 'styled-components'; -import * as stores from 'teleport/Console/stores/types'; import { Terminal, TerminalRef } from 'teleport/Console/DocumentSsh/Terminal'; -import { useMfa } from 'teleport/lib/useMfa'; +import * as stores from 'teleport/Console/stores/types'; +import { useMfaTty } from 'teleport/lib/useMfa'; import Document from 'teleport/Console/Document'; import AuthnDialog from 'teleport/components/AuthnDialog'; -import { useDbSession } from './useDbSession'; import { ConnectDialog } from './ConnectDialog'; +import { useDbSession } from './useDbSession'; type Props = { visible: boolean; @@ -38,11 +38,11 @@ type Props = { export function DocumentDb({ doc, visible }: Props) { const terminalRef = useRef(); const { tty, status, closeDocument, sendDbConnectData } = useDbSession(doc); - const mfa = useMfa(tty); + const mfa = useMfaTty(tty); useEffect(() => { // when switching tabs or closing tabs, focus on visible terminal terminalRef.current?.focus(); - }, [visible, mfa.requested, status]); + }, [visible, mfa, status]); const theme = useTheme(); return ( @@ -52,7 +52,7 @@ export function DocumentDb({ doc, visible }: Props) { )} - {mfa.requested && } + {status === 'waiting' && ( (); const { tty, status, closeDocument, sendKubeExecData } = useKubeExecSession(doc); - const mfa = useMfa(tty); + const mfa = useMfaTty(tty); useEffect(() => { // when switching tabs or closing tabs, focus on visible terminal terminalRef.current?.focus(); - }, [visible, mfa.requested]); + }, [visible, mfa.challenge]); const theme = useTheme(); const terminal = ( @@ -63,7 +63,7 @@ export default function DocumentKubeExec({ doc, visible }: Props) { )} - {mfa.requested && } + {status === 'waiting-for-exec-data' && ( diff --git a/web/packages/teleport/src/Console/DocumentSsh/DocumentSsh.tsx b/web/packages/teleport/src/Console/DocumentSsh/DocumentSsh.tsx index c378216dd66fb..4902d90845bf1 100644 --- a/web/packages/teleport/src/Console/DocumentSsh/DocumentSsh.tsx +++ b/web/packages/teleport/src/Console/DocumentSsh/DocumentSsh.tsx @@ -16,31 +16,32 @@ * along with this program. If not, see . */ -import { useRef, useEffect, useState, useCallback } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTheme } from 'styled-components'; -import { Indicator, Box } from 'design'; +import { Box, Indicator } from 'design'; import { - FileTransferActionBar, FileTransfer, - FileTransferRequests, + FileTransferActionBar, FileTransferContextProvider, + FileTransferRequests, } from 'shared/components/FileTransfer'; import { TerminalSearch } from 'shared/components/TerminalSearch'; import * as stores from 'teleport/Console/stores'; import AuthnDialog from 'teleport/components/AuthnDialog'; -import { useMfa } from 'teleport/lib/useMfa'; +import { useMfa, useMfaTty } from 'teleport/lib/useMfa'; +import { MfaChallengeScope } from 'teleport/services/auth/auth'; import Document from '../Document'; import { useConsoleContext } from '../consoleContextProvider'; import { Terminal, TerminalRef } from './Terminal'; -import useSshSession from './useSshSession'; import { useFileTransfer } from './useFileTransfer'; +import useSshSession from './useSshSession'; export default function DocumentSshWrapper(props: PropTypes) { return ( @@ -56,13 +57,15 @@ function DocumentSsh({ doc, visible }: PropTypes) { const terminalRef = useRef(); const { tty, status, closeDocument, session } = useSshSession(doc); const [showSearch, setShowSearch] = useState(false); - const mfa = useMfa(tty); - const { - getMfaResponseAttempt, - getDownloader, - getUploader, - fileTransferRequests, - } = useFileTransfer(tty, session, doc, mfa.addMfaToScpUrls); + + const ttyMfa = useMfaTty(tty); + const ftMfa = useMfa({ + isMfaRequired: ttyMfa.required, + req: { + scope: MfaChallengeScope.USER_SESSION, + }, + }); + const ft = useFileTransfer(tty, session, doc, ftMfa); const theme = useTheme(); function handleCloseFileTransfer() { @@ -75,8 +78,13 @@ function DocumentSsh({ doc, visible }: PropTypes) { useEffect(() => { // when switching tabs or closing tabs, focus on visible terminal - terminalRef.current?.focus(); - }, [visible, mfa.requested]); + if ( + ttyMfa.attempt.status === 'processing' || + ftMfa.attempt.status === 'processing' + ) { + terminalRef.current?.focus(); + } + }, [visible, ttyMfa.attempt.status, ftMfa.attempt.status]); const onSearchClose = useCallback(() => { setShowSearch(false); @@ -110,21 +118,15 @@ function DocumentSsh({ doc, visible }: PropTypes) { } beforeClose={() => window.confirm('Are you sure you want to cancel file transfers?') } - errorText={ - getMfaResponseAttempt.status === 'failed' - ? getMfaResponseAttempt.statusText - : null - } afterClose={handleCloseFileTransfer} transferHandlers={{ - getDownloader, - getUploader, + ...ft, }} /> @@ -143,7 +145,8 @@ function DocumentSsh({ doc, visible }: PropTypes) { )} - {mfa.requested && } + + {status === 'initialized' && terminal} ); diff --git a/web/packages/teleport/src/Console/DocumentSsh/useFileTransfer.ts b/web/packages/teleport/src/Console/DocumentSsh/useFileTransfer.ts index 90c3625a902cf..92c4c9976a198 100644 --- a/web/packages/teleport/src/Console/DocumentSsh/useFileTransfer.ts +++ b/web/packages/teleport/src/Console/DocumentSsh/useFileTransfer.ts @@ -16,18 +16,21 @@ * along with this program. If not, see . */ -import { useEffect, useState, useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useFileTransferContext } from 'shared/components/FileTransfer'; -import Tty from 'teleport/lib/term/tty'; +import { DocumentSsh } from 'teleport/Console/stores'; import { EventType } from 'teleport/lib/term/enums'; +import Tty from 'teleport/lib/term/tty'; import { Session } from 'teleport/services/session'; -import { DocumentSsh } from 'teleport/Console/stores'; + +import cfg from 'teleport/config'; + +import { MfaState } from 'teleport/lib/useMfa'; import { useConsoleContext } from '../consoleContextProvider'; import { getHttpFileTransferHandlers } from './httpFileTransferHandlers'; -import useGetScpUrl from './useGetScpUrl'; export type FileTransferRequest = { sid: string; @@ -51,7 +54,7 @@ export const useFileTransfer = ( tty: Tty, session: Session, currentDoc: DocumentSsh, - addMfaToScpUrls: boolean + mfa: MfaState ) => { const { filesStore } = useFileTransferContext(); const startTransfer = filesStore.start; @@ -60,8 +63,6 @@ export const useFileTransfer = ( const [fileTransferRequests, setFileTransferRequests] = useState< FileTransferRequest[] >([]); - const { getScpUrl, attempt: getMfaResponseAttempt } = - useGetScpUrl(addMfaToScpUrls); const { clusterId, serverId, login } = currentDoc; const download = useCallback( @@ -70,7 +71,8 @@ export const useFileTransfer = ( abortController: AbortController, moderatedSessionParams?: ModeratedSessionParams ) => { - const url = await getScpUrl({ + const mfaResponse = await mfa.getChallengeResponse(); + const url = cfg.getScpUrl({ location, clusterId, serverId, @@ -78,7 +80,9 @@ export const useFileTransfer = ( filename: location, moderatedSessionId: moderatedSessionParams?.moderatedSessionId, fileTransferRequestId: moderatedSessionParams?.fileRequestId, + mfaResponse, }); + if (!url) { // if we return nothing here, the file transfer will not be added to the // file transfer list. If we add it to the list, the file will continue to @@ -88,7 +92,7 @@ export const useFileTransfer = ( } return getHttpFileTransferHandlers().download(url, abortController); }, - [clusterId, login, serverId, getScpUrl] + [clusterId, login, serverId, mfa] ); const upload = useCallback( @@ -98,7 +102,9 @@ export const useFileTransfer = ( abortController: AbortController, moderatedSessionParams?: ModeratedSessionParams ) => { - const url = await getScpUrl({ + const mfaResponse = await mfa.getChallengeResponse(); + + const url = cfg.getScpUrl({ location, clusterId, serverId, @@ -106,6 +112,7 @@ export const useFileTransfer = ( filename: file.name, moderatedSessionId: moderatedSessionParams?.moderatedSessionId, fileTransferRequestId: moderatedSessionParams?.fileRequestId, + mfaResponse, }); if (!url) { // if we return nothing here, the file transfer will not be added to the @@ -116,7 +123,7 @@ export const useFileTransfer = ( } return getHttpFileTransferHandlers().upload(url, file, abortController); }, - [clusterId, serverId, login, getScpUrl] + [clusterId, serverId, login, mfa] ); /* @@ -256,7 +263,6 @@ export const useFileTransfer = ( return { fileTransferRequests, - getMfaResponseAttempt, getUploader, getDownloader, }; diff --git a/web/packages/teleport/src/Console/DocumentSsh/useGetScpUrl.ts b/web/packages/teleport/src/Console/DocumentSsh/useGetScpUrl.ts deleted file mode 100644 index 478ccbcc5fa59..0000000000000 --- a/web/packages/teleport/src/Console/DocumentSsh/useGetScpUrl.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { useCallback } from 'react'; -import useAttempt from 'shared/hooks/useAttemptNext'; - -import cfg, { UrlScpParams } from 'teleport/config'; -import auth, { MfaChallengeScope } from 'teleport/services/auth/auth'; - -export default function useGetScpUrl(addMfaToScpUrls: boolean) { - const { setAttempt, attempt, handleError } = useAttempt(''); - - const getScpUrl = useCallback( - async (params: UrlScpParams) => { - setAttempt({ - status: 'processing', - statusText: '', - }); - if (!addMfaToScpUrls) { - return cfg.getScpUrl(params); - } - try { - const challenge = await auth.getMfaChallenge({ - scope: MfaChallengeScope.USER_SESSION, - }); - - const response = await auth.getMfaChallengeResponse( - challenge, - 'webauthn' - ); - - setAttempt({ - status: 'success', - statusText: '', - }); - return cfg.getScpUrl({ - webauthn: response.webauthn_response, - ...params, - }); - } catch (error) { - handleError(error); - } - }, - [addMfaToScpUrls, handleError, setAttempt] - ); - - return { - getScpUrl, - attempt, - }; -} diff --git a/web/packages/teleport/src/DesktopSession/DesktopSession.story.tsx b/web/packages/teleport/src/DesktopSession/DesktopSession.story.tsx index 97606b1ea3b86..e401ab43de9f1 100644 --- a/web/packages/teleport/src/DesktopSession/DesktopSession.story.tsx +++ b/web/packages/teleport/src/DesktopSession/DesktopSession.story.tsx @@ -16,16 +16,16 @@ * along with this program. If not, see . */ -import { useState } from 'react'; import { ButtonPrimary } from 'design/Button'; +import { useState } from 'react'; import { NotificationItem } from 'shared/components/Notification'; import { throttle } from 'shared/utils/highbar'; import { TdpClient, TdpClientEvent } from 'teleport/lib/tdp'; import { makeDefaultMfaState } from 'teleport/lib/useMfa'; -import { State } from './useDesktopSession'; import { DesktopSession } from './DesktopSession'; +import { State } from './useDesktopSession'; export default { title: 'Teleport/DesktopSession', @@ -261,14 +261,17 @@ export const WebAuthnPrompt = () => ( }} wsConnection={{ status: 'open' }} mfa={{ - errorText: '', - requested: true, - setErrorText: () => null, - addMfaToScpUrls: false, - onWebauthnAuthenticate: () => null, - onSsoAuthenticate: () => null, - webauthnPublicKey: null, - ssoChallenge: null, + ...makeDefaultMfaState(), + attempt: { + status: 'processing', + statusText: '', + data: null, + }, + challenge: { + webauthnPublicKey: { + challenge: new ArrayBuffer(1), + }, + }, }} /> ); diff --git a/web/packages/teleport/src/DesktopSession/DesktopSession.tsx b/web/packages/teleport/src/DesktopSession/DesktopSession.tsx index f5105f7d0246e..851c72b769fe4 100644 --- a/web/packages/teleport/src/DesktopSession/DesktopSession.tsx +++ b/web/packages/teleport/src/DesktopSession/DesktopSession.tsx @@ -184,12 +184,10 @@ export function DesktopSession(props: State) { const MfaDialog = ({ mfa }: { mfa: MfaState }) => { return ( { - mfa.setErrorText( - 'This session requires multi factor authentication to continue. Please hit "Retry" and follow the prompts given by your browser to complete authentication.' - ); - }} + mfaState={mfa} + replaceErrorText={ + 'This session requires multi factor authentication to continue. Please hit try again and follow the prompts given by your browser to complete authentication.' + } /> ); }; @@ -294,7 +292,7 @@ const nextScreenState = ( // Otherwise, calculate a new screen state. const showAnotherSessionActive = showAnotherSessionActiveDialog; - const showMfa = webauthn.requested; + const showMfa = webauthn.challenge; const showAlert = fetchAttempt.status === 'failed' || // Fetch attempt failed tdpConnection.status === 'failed' || // TDP connection failed diff --git a/web/packages/teleport/src/DesktopSession/useDesktopSession.tsx b/web/packages/teleport/src/DesktopSession/useDesktopSession.tsx index 1f642d38d8d96..f14482669f471 100644 --- a/web/packages/teleport/src/DesktopSession/useDesktopSession.tsx +++ b/web/packages/teleport/src/DesktopSession/useDesktopSession.tsx @@ -22,7 +22,7 @@ import { useParams } from 'react-router'; import useAttempt from 'shared/hooks/useAttemptNext'; import { ButtonState } from 'teleport/lib/tdp'; -import { useMfa } from 'teleport/lib/useMfa'; +import { useMfaTty } from 'teleport/lib/useMfa'; import desktopService from 'teleport/services/desktops'; import userService from 'teleport/services/user'; @@ -130,7 +130,7 @@ export default function useDesktopSession() { }); const tdpClient = clientCanvasProps.tdpClient; - const mfa = useMfa(tdpClient); + const mfa = useMfaTty(tdpClient); const onShareDirectory = () => { try { diff --git a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.story.tsx b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.story.tsx index 0e493d383efb4..7137b983a4d23 100644 --- a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.story.tsx +++ b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.story.tsx @@ -26,21 +26,27 @@ export default { export const LoadedWithMultipleOptions = () => { const props: Props = { - ...defaultProps, - mfa: { - ...defaultProps.mfa, - ssoChallenge: { - redirectUrl: 'hi', - requestId: '123', - channelId: '123', - device: { - connectorId: '123', - connectorType: 'saml', - displayName: 'Okta', - }, + mfaState: { + ...makeDefaultMfaState(), + attempt: { + status: 'processing', + statusText: '', + data: null, }, - webauthnPublicKey: { - challenge: new ArrayBuffer(1), + challenge: { + ssoChallenge: { + redirectUrl: 'hi', + requestId: '123', + channelId: '123', + device: { + connectorId: '123', + connectorType: 'saml', + displayName: 'Okta', + }, + }, + webauthnPublicKey: { + challenge: new ArrayBuffer(1), + }, }, }, }; @@ -49,29 +55,35 @@ export const LoadedWithMultipleOptions = () => { export const LoadedWithSingleOption = () => { const props: Props = { - ...defaultProps, - mfa: { - ...defaultProps.mfa, - webauthnPublicKey: { - challenge: new ArrayBuffer(1), + mfaState: { + ...makeDefaultMfaState(), + attempt: { + status: 'processing', + statusText: '', + data: null, + }, + challenge: { + webauthnPublicKey: { + challenge: new ArrayBuffer(1), + }, }, }, }; return ; }; -export const Error = () => { +export const LoadedWithError = () => { + const err = new Error('Something went wrong'); const props: Props = { - ...defaultProps, - mfa: { - ...defaultProps.mfa, - errorText: 'Something went wrong', + mfaState: { + ...makeDefaultMfaState(), + attempt: { + status: 'error', + statusText: err.message, + error: err, + data: null, + }, }, }; return ; }; - -const defaultProps: Props = { - mfa: makeDefaultMfaState(), - onCancel: () => null, -}; diff --git a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.test.tsx b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.test.tsx index 516c021c8d452..34be98660bc39 100644 --- a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.test.tsx +++ b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.test.tsx @@ -16,15 +16,15 @@ * along with this program. If not, see . */ -import { render, screen, fireEvent } from 'design/utils/testing'; +import { fireEvent, render, screen } from 'design/utils/testing'; import { makeDefaultMfaState, MfaState } from 'teleport/lib/useMfa'; -import { SSOChallenge } from 'teleport/services/mfa'; +import { getMfaChallengeOptions, SsoChallenge } from 'teleport/services/mfa'; import AuthnDialog from './AuthnDialog'; -const mockSsoChallenge: SSOChallenge = { +const mockSsoChallenge: SsoChallenge = { redirectUrl: 'url', requestId: '123', device: { @@ -51,8 +51,17 @@ describe('AuthnDialog', () => { }); test('renders single option dialog', () => { - const mfa = makeMockState({ ssoChallenge: mockSsoChallenge }); - render(); + const mfa = makeMockState({ + challenge: { + ssoChallenge: mockSsoChallenge, + }, + attempt: { + status: 'processing', + statusText: '', + data: null, + }, + }); + render(); expect(screen.getByText('Verify Your Identity')).toBeInTheDocument(); expect( @@ -63,13 +72,22 @@ describe('AuthnDialog', () => { }); test('renders multi option dialog', () => { - const mfa = makeMockState({ + const challenge = { ssoChallenge: mockSsoChallenge, webauthnPublicKey: { challenge: new ArrayBuffer(1), }, + }; + const mfa = makeMockState({ + options: getMfaChallengeOptions(challenge), + challenge, + attempt: { + status: 'processing', + statusText: '', + data: null, + }, }); - render(); + render(); expect(screen.getByText('Verify Your Identity')).toBeInTheDocument(); expect( @@ -83,8 +101,16 @@ describe('AuthnDialog', () => { test('displays error text when provided', () => { const errorText = 'Authentication failed'; - const mfa = makeMockState({ errorText }); - render(); + const mfa = makeMockState({ + challenge: {}, + attempt: { + status: 'error', + statusText: errorText, + data: null, + error: new Error(errorText), + }, + }); + render(); expect(screen.getByTestId('danger-alert')).toBeInTheDocument(); expect(screen.getByText(errorText)).toBeInTheDocument(); @@ -92,23 +118,37 @@ describe('AuthnDialog', () => { test('sso button renders with callback', async () => { const mfa = makeMockState({ - ssoChallenge: mockSsoChallenge, - onSsoAuthenticate: jest.fn(), + challenge: { + ssoChallenge: mockSsoChallenge, + }, + attempt: { + status: 'processing', + statusText: '', + data: null, + }, + submit: jest.fn(), }); - render(); + render(); const ssoButton = screen.getByText('Okta'); fireEvent.click(ssoButton); - expect(mfa.onSsoAuthenticate).toHaveBeenCalledTimes(1); + expect(mfa.submit).toHaveBeenCalledTimes(1); }); test('webauthn button renders with callback', async () => { const mfa = makeMockState({ - webauthnPublicKey: { challenge: new ArrayBuffer(0) }, - onWebauthnAuthenticate: jest.fn(), + challenge: { + webauthnPublicKey: { challenge: new ArrayBuffer(0) }, + }, + attempt: { + status: 'processing', + statusText: '', + data: null, + }, + submit: jest.fn(), }); - render(); + render(); const webauthn = screen.getByText('Passkey or MFA Device'); fireEvent.click(webauthn); - expect(mfa.onWebauthnAuthenticate).toHaveBeenCalledTimes(1); + expect(mfa.submit).toHaveBeenCalledTimes(1); }); }); diff --git a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx index 0a301b1f16c43..1b862601beaf1 100644 --- a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx +++ b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx @@ -16,76 +16,101 @@ * along with this program. If not, see . */ -import Dialog, { DialogContent } from 'design/Dialog'; import { Danger } from 'design/Alert'; -import { FingerprintSimple, Cross } from 'design/Icon'; +import Dialog, { DialogContent } from 'design/Dialog'; +import { Cross, FingerprintSimple } from 'design/Icon'; -import { Text, ButtonSecondary, Flex, ButtonIcon, H2 } from 'design'; +import { ButtonIcon, ButtonSecondary, Flex, H2, Text } from 'design'; import { guessProviderType } from 'shared/components/ButtonSso'; import { SSOIcon } from 'shared/components/ButtonSso/ButtonSso'; import { MfaState } from 'teleport/lib/useMfa'; +import { MFA_OPTION_TOTP } from 'teleport/services/mfa'; + +export type Props = { + mfaState: MfaState; + replaceErrorText?: string; + onClose?: () => void; +}; + +export default function AuthnDialog({ + mfaState: { options, challenge, submit, attempt, resetAttempt }, + replaceErrorText, + onClose, +}: Props) { + if (!challenge && attempt.status !== 'error') return; -export default function AuthnDialog({ mfa, onCancel }: Props) { - let hasMultipleOptions = mfa.ssoChallenge && mfa.webauthnPublicKey; + // TODO(Joerger): TOTP should be pretty easy to support here with a small button -> form flow. + const onlyTotpAvailable = + options?.length === 1 && options[0] === MFA_OPTION_TOTP; return ( ({ width: '400px' })} open={true}>

    Verify Your Identity

    - + { + resetAttempt(); + onClose(); + }} + >
    - {mfa.errorText && ( + {onlyTotpAvailable && ( - {mfa.errorText} + { + 'Authenticator app is not currently supported for this action, please register a passkey or a security key to continue.' + } + + )} + {attempt.status === 'error' && ( + + {replaceErrorText || attempt.statusText} )} - {hasMultipleOptions + {options?.length > 1 ? 'Select one of the following methods to verify your identity:' : 'Select the method below to verify your identity:'} - - {mfa.ssoChallenge && ( - - - {mfa.ssoChallenge.device.displayName || - mfa.ssoChallenge.device.connectorId} - - )} - {mfa.webauthnPublicKey && ( - - - Passkey or MFA Device - - )} - + {challenge && ( + + {challenge.ssoChallenge && ( + submit('sso')} + gap={2} + block + > + + {challenge.ssoChallenge.device.displayName || + challenge.ssoChallenge.device.connectorId} + + )} + {challenge.webauthnPublicKey && ( + submit('webauthn')} + gap={2} + block + > + + Passkey or MFA Device + + )} + + )}
    ); } - -export type Props = { - mfa: MfaState; - onCancel: () => void; -}; diff --git a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts index ed8c73f3fe6da..62291b5a1f6a7 100644 --- a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts +++ b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts @@ -38,12 +38,15 @@ export default function useReAuthenticate({ const [mfaOptions, setMfaOptions] = useState(); const [challengeState, setChallengeState] = useState(); + function setMfaChallenge(challenge: MfaAuthenticateChallenge) { + setChallengeState({ challenge, deviceUsage: 'mfa' }); + } + const [initAttempt, init] = useAsync(async () => { const challenge = await auth.getMfaChallenge({ scope: challengeScope, }); - - setChallengeState({ challenge, deviceUsage: 'mfa' }); + setMfaChallenge(challenge); setMfaOptions(getMfaChallengeOptions(challenge)); }); @@ -112,6 +115,7 @@ export default function useReAuthenticate({ return { initAttempt, mfaOptions, + setMfaChallenge, submitWithMfa, submitAttempt, clearSubmitAttempt, @@ -126,6 +130,7 @@ export type ReauthProps = { export type ReauthState = { initAttempt: Attempt; mfaOptions: MfaOption[]; + setMfaChallenge: (challenge: MfaAuthenticateChallenge) => void; submitWithMfa: ( mfaType?: DeviceType, deviceUsage?: DeviceUsage, diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index f69a468f121fe..5d1792b2abb3d 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -17,9 +17,7 @@ */ import { generatePath } from 'react-router'; - import { IncludedResourceMode } from 'shared/components/UnifiedResources'; - import { mergeDeep } from 'shared/utils/highbar'; import { @@ -43,10 +41,10 @@ import type { import type { SortType } from 'teleport/services/agents'; import type { KubeResourceKind } from 'teleport/services/kube/types'; -import type { WebauthnAssertionResponse } from 'teleport/services/mfa'; import type { RecordingType } from 'teleport/services/recordings'; import type { ParticipantMode } from 'teleport/services/session'; import type { YamlSupportedResourceKind } from 'teleport/services/yaml/types'; +import type { MfaChallengeResponse } from './services/mfa'; const cfg = { /** @deprecated Use cfg.edition instead. */ @@ -885,20 +883,25 @@ const cfg = { }); }, - getScpUrl({ webauthn, ...params }: UrlScpParams) { + getScpUrl({ mfaResponse, ...params }: UrlScpParams) { let path = generatePath(cfg.api.scp, { ...params, }); - if (!webauthn) { + if (!mfaResponse) { return path; } // non-required MFA will mean this param is undefined and generatePath doesn't like undefined // or optional params. So we append it ourselves here. Its ok to be undefined when sent to the server // as the existence of this param is what will issue certs - return `${path}&webauthn=${JSON.stringify({ - webauthnAssertionResponse: webauthn, + + // TODO(Joerger): DELETE IN v19.0.0 + // We include webauthn for backwards compatibility. + path = `${path}&webauthn=${JSON.stringify({ + webauthnAssertionResponse: mfaResponse.webauthn_response, })}`; + + return `${path}&mfaResponse=${JSON.stringify(mfaResponse)}`; }, getRenewTokenUrl() { @@ -1246,6 +1249,14 @@ export interface UrlAppParams { arn?: string; } +export interface CreateAppSessionParams { + fqdn: string; + clusterId?: string; + publicAddr?: string; + arn?: string; + mfaResponse?: MfaChallengeResponse; +} + export interface UrlScpParams { clusterId: string; serverId: string; @@ -1254,7 +1265,7 @@ export interface UrlScpParams { filename: string; moderatedSessionId?: string; fileTransferRequestId?: string; - webauthn?: WebauthnAssertionResponse; + mfaResponse?: MfaChallengeResponse; } export interface UrlSshParams { diff --git a/web/packages/teleport/src/lib/EventEmitterMfaSender.ts b/web/packages/teleport/src/lib/EventEmitterMfaSender.ts index da30f1201e0c9..2753251121061 100644 --- a/web/packages/teleport/src/lib/EventEmitterMfaSender.ts +++ b/web/packages/teleport/src/lib/EventEmitterMfaSender.ts @@ -18,10 +18,7 @@ import { EventEmitter } from 'events'; -import { - MfaChallengeResponse, - WebauthnAssertionResponse, -} from 'teleport/services/mfa'; +import { MfaChallengeResponse } from 'teleport/services/mfa'; class EventEmitterMfaSender extends EventEmitter { constructor() { @@ -32,15 +29,6 @@ class EventEmitterMfaSender extends EventEmitter { sendChallengeResponse(data: MfaChallengeResponse) { throw new Error('Not implemented'); } - - // TODO (avatus) DELETE IN 18 - /** - * @deprecated Use sendChallengeResponse instead. - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - sendWebAuthn(data: WebauthnAssertionResponse) { - throw new Error('Not implemented'); - } } export { EventEmitterMfaSender }; diff --git a/web/packages/teleport/src/lib/tdp/client.ts b/web/packages/teleport/src/lib/tdp/client.ts index ca18c58744124..b6ab1264b185d 100644 --- a/web/packages/teleport/src/lib/tdp/client.ts +++ b/web/packages/teleport/src/lib/tdp/client.ts @@ -57,7 +57,6 @@ import type { SyncKeys, SharedDirectoryTruncateResponse, } from './codec'; -import type { WebauthnAssertionResponse } from 'teleport/services/mfa'; export enum TdpClientEvent { TDP_CLIENT_SCREEN_SPEC = 'tdp client screen spec', @@ -624,14 +623,6 @@ export default class Client extends EventEmitterMfaSender { this.send(this.codec.encodeClipboardData(clipboardData)); } - sendWebAuthn(data: WebauthnAssertionResponse) { - const msg = this.codec.encodeMfaJson({ - mfaType: 'n', - jsonString: JSON.stringify(data), - }); - this.send(msg); - } - addSharedDirectory(sharedDirectory: FileSystemDirectoryHandle) { try { this.sdManager.add(sharedDirectory); diff --git a/web/packages/teleport/src/lib/term/tty.ts b/web/packages/teleport/src/lib/term/tty.ts index 3e924ff466f3f..a78fafb1ebd0d 100644 --- a/web/packages/teleport/src/lib/term/tty.ts +++ b/web/packages/teleport/src/lib/term/tty.ts @@ -19,7 +19,6 @@ import Logger from 'shared/libs/logger'; import { EventEmitterMfaSender } from 'teleport/lib/EventEmitterMfaSender'; -import { WebauthnAssertionResponse } from 'teleport/services/mfa'; import { AuthenticatedWebSocket } from 'teleport/lib/AuthenticatedWebSocket'; import { MfaChallengeResponse } from 'teleport/services/mfa'; @@ -88,7 +87,7 @@ class Tty extends EventEmitterMfaSender { // but to be backward compatible, we need to still spread the existing webauthn only fields // as "top level" fields so old proxies can still respond to webauthn challenges. // in 19, we can just pass "data" without this extra step - // TODO (avatus): DELETE IN 18 + // TODO (avatus): DELETE IN 19.0.0 const backwardCompatibleData = { ...data.webauthn_response, ...data, @@ -100,16 +99,6 @@ class Tty extends EventEmitterMfaSender { this.socket.send(bytearray); } - // TODO (avatus) DELETE IN 18 - /** - * @deprecated Use sendChallengeResponse instead. - */ - sendWebAuthn(data: WebauthnAssertionResponse) { - const encoded = this._proto.encodeChallengeResponse(JSON.stringify(data)); - const bytearray = new Uint8Array(encoded); - this.socket.send(bytearray); - } - sendKubeExecData(data: KubeExecData) { const encoded = this._proto.encodeKubeExecData(JSON.stringify(data)); const bytearray = new Uint8Array(encoded); diff --git a/web/packages/teleport/src/lib/useMfa.test.tsx b/web/packages/teleport/src/lib/useMfa.test.tsx new file mode 100644 index 0000000000000..886fbff25a662 --- /dev/null +++ b/web/packages/teleport/src/lib/useMfa.test.tsx @@ -0,0 +1,246 @@ +/** + * 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 . + */ + +import auth, { MfaChallengeScope } from 'teleport/services/auth/auth'; + +import { renderHook, waitFor } from '@testing-library/react'; +import { useState } from 'react'; + +import { CreateAuthenticateChallengeRequest } from 'teleport/services/auth'; +import { + MFA_OPTION_WEBAUTHN, + MfaAuthenticateChallenge, + MfaChallengeResponse, +} from 'teleport/services/mfa'; + +import { useMfa } from './useMfa'; + +const mockChallenge: MfaAuthenticateChallenge = { + webauthnPublicKey: {} as PublicKeyCredentialRequestOptions, +}; + +const mockResponse: MfaChallengeResponse = { + webauthn_response: { + id: 'cred-id', + type: 'public-key', + extensions: { + appid: true, + }, + rawId: 'rawId', + response: { + authenticatorData: 'authenticatorData', + clientDataJSON: 'clientDataJSON', + signature: 'signature', + userHandle: 'userHandle', + }, + }, +}; + +const mockChallengeReq: CreateAuthenticateChallengeRequest = { + scope: MfaChallengeScope.USER_SESSION, + isMfaRequiredRequest: { + node: { + node_name: 'node', + login: 'login', + }, + }, +}; + +describe('useMfa', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('mfa required', async () => { + jest.spyOn(auth, 'getMfaChallenge').mockResolvedValueOnce(mockChallenge); + jest + .spyOn(auth, 'getMfaChallengeResponse') + .mockResolvedValueOnce(mockResponse); + const { result: mfa } = renderHook(() => + useMfa({ + req: mockChallengeReq, + }) + ); + + const respPromise = mfa.current.getChallengeResponse(); + await waitFor(() => { + expect(auth.getMfaChallenge).toHaveBeenCalledWith(mockChallengeReq); + }); + + expect(mfa.current.options).toEqual([MFA_OPTION_WEBAUTHN]); + expect(mfa.current.required).toEqual(true); + expect(mfa.current.challenge).toEqual(mockChallenge); + expect(mfa.current.attempt.status).toEqual('processing'); + + await mfa.current.submit('webauthn'); + await waitFor(() => { + expect(auth.getMfaChallengeResponse).toHaveBeenCalledWith( + mockChallenge, + 'webauthn', + undefined + ); + }); + + const resp = await respPromise; + expect(resp).toEqual(mockResponse); + expect(mfa.current.challenge).toEqual(null); + expect(mfa.current.attempt.status).toEqual('success'); + }); + + test('mfa not required', async () => { + jest.spyOn(auth, 'getMfaChallenge').mockResolvedValue(null); + + const { result: mfa } = renderHook(() => + useMfa({ + req: mockChallengeReq, + }) + ); + + // If a challenge is not returned, an empty mfa response should be returned + // early and the requirement changed to false for future calls. + const resp = await mfa.current.getChallengeResponse(); + expect(auth.getMfaChallenge).toHaveBeenCalledWith(mockChallengeReq); + expect(resp).toEqual(undefined); + await waitFor(() => expect(mfa.current.required).toEqual(false)); + }); + + test('adaptable mfa requirement state', async () => { + jest.spyOn(auth, 'getMfaChallenge').mockResolvedValue(null); + + let isMfaRequired: boolean; + let setMfaRequired: (b: boolean) => void; + + let req: CreateAuthenticateChallengeRequest; + let setReq: (r: CreateAuthenticateChallengeRequest) => void; + + const { result: mfa } = renderHook(() => { + [isMfaRequired, setMfaRequired] = useState(null); + [req, setReq] = + useState(mockChallengeReq); + + return useMfa({ + req: req, + isMfaRequired: isMfaRequired, + }); + }); + + // mfaRequired should change when the isMfaRequired arg changes, allowing + // callers to propagate mfa required late (e.g. per-session MFA for file transfers) + setMfaRequired(false); + await waitFor(() => expect(mfa.current.required).toEqual(false)); + + setMfaRequired(true); + await waitFor(() => expect(mfa.current.required).toEqual(true)); + + setMfaRequired(null); + await waitFor(() => expect(mfa.current.required).toEqual(null)); + + // If isMfaRequiredRequest changes, the mfaRequired value should be reset. + setReq({ + ...mockChallengeReq, + isMfaRequiredRequest: { + admin_action: {}, + }, + }); + await waitFor(() => expect(mfa.current.required).toEqual(null)); + }); + + test('mfa challenge error', async () => { + const err = new Error('an error has occurred'); + jest.spyOn(auth, 'getMfaChallenge').mockImplementation(() => { + throw err; + }); + + const { result: mfa } = renderHook(() => useMfa({})); + + await expect(mfa.current.getChallengeResponse).rejects.toThrow(err); + await waitFor(() => { + expect(mfa.current.attempt).toEqual({ + status: 'error', + statusText: err.message, + error: err, + data: null, + }); + }); + }); + + test('mfa response error', async () => { + const err = new Error('an error has occurred'); + jest.spyOn(auth, 'getMfaChallenge').mockResolvedValueOnce(mockChallenge); + jest.spyOn(auth, 'getMfaChallengeResponse').mockImplementation(async () => { + throw err; + }); + + const { result: mfa } = renderHook(() => + useMfa({ + req: mockChallengeReq, + }) + ); + + const respPromise = mfa.current.getChallengeResponse(); + await waitFor(() => { + expect(auth.getMfaChallenge).toHaveBeenCalledWith(mockChallengeReq); + }); + await mfa.current.submit('webauthn'); + + await waitFor(() => { + expect(mfa.current.attempt).toEqual({ + status: 'error', + statusText: err.message, + error: err, + data: null, + }); + }); + + // After an error, the mfa response promise remains in an unresolved state, + // allowing for retries. + jest + .spyOn(auth, 'getMfaChallengeResponse') + .mockResolvedValueOnce(mockResponse); + await mfa.current.submit('webauthn'); + expect(await respPromise).toEqual(mockResponse); + }); + + test('reset mfa attempt', async () => { + jest.spyOn(auth, 'getMfaChallenge').mockResolvedValue(mockChallenge); + const { result: mfa } = renderHook(() => + useMfa({ + req: mockChallengeReq, + }) + ); + + const respPromise = mfa.current.getChallengeResponse(); + await waitFor(() => { + expect(auth.getMfaChallenge).toHaveBeenCalled(); + }); + + mfa.current.resetAttempt(); + + await expect(respPromise).rejects.toThrow( + new Error('MFA attempt cancelled by user') + ); + + await waitFor(() => { + expect(mfa.current.attempt.status).toEqual('error'); + }); + }); +}); diff --git a/web/packages/teleport/src/lib/useMfa.ts b/web/packages/teleport/src/lib/useMfa.ts index 664016790e002..50f852c3768e3 100644 --- a/web/packages/teleport/src/lib/useMfa.ts +++ b/web/packages/teleport/src/lib/useMfa.ts @@ -16,220 +16,208 @@ * along with this program. If not, see . */ -import { useState, useEffect, useCallback } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { Attempt, makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync'; import { EventEmitterMfaSender } from 'teleport/lib/EventEmitterMfaSender'; import { TermEvent } from 'teleport/lib/term/enums'; -import { parseMfaChallengeJson as parseMfaChallenge } from 'teleport/services/mfa/makeMfa'; import { - MfaAuthenticateChallengeJson, - SSOChallenge, -} from 'teleport/services/mfa'; + CreateAuthenticateChallengeRequest, + parseMfaChallengeJson, +} from 'teleport/services/auth'; import auth from 'teleport/services/auth/auth'; +import { + DeviceType, + getMfaChallengeOptions, + MfaAuthenticateChallenge, + MfaChallengeResponse, + MfaOption, +} from 'teleport/services/mfa'; -export function useMfa(emitterSender: EventEmitterMfaSender): MfaState { - const [state, setState] = useState<{ - errorText: string; - addMfaToScpUrls: boolean; - webauthnPublicKey: PublicKeyCredentialRequestOptions; - ssoChallenge: SSOChallenge; - totpChallenge: boolean; - }>({ - addMfaToScpUrls: false, - errorText: '', - webauthnPublicKey: null, - ssoChallenge: null, - totpChallenge: false, - }); - - function clearChallenges() { - setState(prevState => ({ - ...prevState, - totpChallenge: false, - webauthnPublicKey: null, - ssoChallenge: null, - })); - } - - function onSsoAuthenticate() { - if (!state.ssoChallenge) { - setState(prevState => ({ - ...prevState, - errorText: 'Invalid or missing SSO challenge', - })); - return; - } - - // try to center the screen - const width = 1045; - const height = 550; - const left = (screen.width - width) / 2; - const top = (screen.height - height) / 2; - - // these params will open a tiny window. - const params = `width=${width},height=${height},left=${left},top=${top}`; - window.open(state.ssoChallenge.redirectUrl, '_blank', params); - } - - function onWebauthnAuthenticate() { - if (!window.PublicKeyCredential) { - const errorText = - 'This browser does not support WebAuthn required for hardware tokens, \ - please try the latest version of Chrome, Firefox or Safari.'; - - setState({ - ...state, - errorText, - }); - return; - } - - auth - .getMfaChallengeResponse({ - webauthnPublicKey: state.webauthnPublicKey, - }) - .then(res => { - setState(prevState => ({ - ...prevState, - errorText: '', - webauthnPublicKey: null, - })); - emitterSender.sendWebAuthn(res.webauthn_response); - }) - .catch((err: Error) => { - setErrorText(err.message); - }); - } - - const waitForSsoChallengeResponse = useCallback( - async ( - ssoChallenge: SSOChallenge, - abortSignal: AbortSignal - ): Promise => { - const channel = new BroadcastChannel(ssoChallenge.channelId); +export type MfaProps = { + req?: CreateAuthenticateChallengeRequest; + isMfaRequired?: boolean | null; +}; - try { - const event = await waitForMessage(channel, abortSignal); - emitterSender.sendChallengeResponse({ - sso_response: { - requestId: ssoChallenge.requestId, - token: event.data.mfaToken, - }, +type mfaResponsePromiseWithResolvers = { + promise: Promise; + resolve: (v: MfaChallengeResponse) => void; + reject: (v?: any) => void; +}; + +/** + * Use the returned object to request MFA checks with a shared state. + * When MFA authentication is in progress, the object's properties can + * be used to display options to the user and prompt for them to complete + * the MFA check. + */ +export function useMfa({ req, isMfaRequired }: MfaProps): MfaState { + const [mfaRequired, setMfaRequired] = useState(); + const [options, setMfaOptions] = useState(); + const [challenge, setMfaChallenge] = useState(); + + const mfaResponsePromiseWithResolvers = + useRef(); + + useEffect(() => { + setMfaRequired(isMfaRequired); + }, [isMfaRequired]); + + useEffect(() => { + setMfaRequired(null); + }, [req?.isMfaRequiredRequest]); + + // getResponse is used to initiate MFA authentication. + // 1. Check if MFA is required by getting a new MFA challenge + // 2. If MFA is required, set the challenge in the MFA state and wait for it to + // be resolved by the caller. + // 3. The caller sees the mfa challenge set in state and submits an mfa response + // request with arguments provided by the user (mfa type, otp code). + // 4. Receive the mfa response through the mfaResponsePromise ref and return it. + // + // The caller should also display errors seen in attempt. + + const [attempt, getResponse, setMfaAttempt] = useAsync( + useCallback( + async (challenge?: MfaAuthenticateChallenge) => { + // If a previous call determined that MFA is not required, this is a noop. + if (mfaRequired === false) return; + + challenge = challenge ? challenge : await auth.getMfaChallenge(req); + if (!challenge) { + setMfaRequired(false); + return; + } + + // Set mfa requirement and options after we get a challenge for the first time. + if (!mfaRequired) setMfaRequired(true); + if (!options) setMfaOptions(getMfaChallengeOptions(challenge)); + + // Prepare a new promise to collect the mfa response retrieved + // through the submit function. + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; }); - clearChallenges(); - } catch (error) { - if (error.name !== 'AbortError') { - throw error; + + mfaResponsePromiseWithResolvers.current = { + promise, + resolve, + reject, + }; + + setMfaChallenge(challenge); + try { + return await promise; + } finally { + mfaResponsePromiseWithResolvers.current = null; + setMfaChallenge(null); } - } finally { - channel.close(); - } + }, + [req, mfaResponsePromiseWithResolvers, options, mfaRequired] + ) + ); + + const resetAttempt = () => { + if (mfaResponsePromiseWithResolvers.current) + mfaResponsePromiseWithResolvers.current.reject( + new Error('MFA attempt cancelled by user') + ); + mfaResponsePromiseWithResolvers.current = null; + setMfaChallenge(null); + setMfaAttempt(makeEmptyAttempt()); + }; + + const getChallengeResponse = useCallback( + async (challenge?: MfaAuthenticateChallenge) => { + const [resp, err] = await getResponse(challenge); + if (err) throw err; + return resp; }, - [emitterSender] + [getResponse] ); - useEffect(() => { - let ssoChallengeAbortController: AbortController | undefined; - const challengeHandler = (challengeJson: string) => { - const challenge = JSON.parse( - challengeJson - ) as MfaAuthenticateChallengeJson; - - const { webauthnPublicKey, ssoChallenge, totpChallenge } = - parseMfaChallenge(challenge); - - setState(prevState => ({ - ...prevState, - addMfaToScpUrls: true, - ssoChallenge, - webauthnPublicKey, - totpChallenge, - })); - - if (ssoChallenge) { - ssoChallengeAbortController?.abort(); - ssoChallengeAbortController = new AbortController(); - void waitForSsoChallengeResponse( - ssoChallenge, - ssoChallengeAbortController.signal + const submit = useCallback( + async (mfaType?: DeviceType, totpCode?: string) => { + if (!mfaResponsePromiseWithResolvers.current) { + throw new Error('submit called without an in flight MFA attempt'); + } + + try { + await mfaResponsePromiseWithResolvers.current.resolve( + await auth.getMfaChallengeResponse(challenge, mfaType, totpCode) ); + } catch (err) { + setMfaAttempt({ + data: null, + status: 'error', + statusText: err.message, + error: err, + }); } + }, + [challenge, mfaResponsePromiseWithResolvers, setMfaAttempt] + ); + + return { + required: mfaRequired, + options, + challenge, + getChallengeResponse, + submit, + attempt, + resetAttempt, + }; +} + +export function useMfaTty(emitterSender: EventEmitterMfaSender): MfaState { + const [mfaRequired, setMfaRequired] = useState(false); + + const mfa = useMfa({ isMfaRequired: mfaRequired }); + + useEffect(() => { + const challengeHandler = async (challengeJson: string) => { + // set Mfa required for other uses of this MfaState (e.g. file transfers) + setMfaRequired(true); + + const challenge = parseMfaChallengeJson(JSON.parse(challengeJson)); + const resp = await mfa.getChallengeResponse(challenge); + emitterSender.sendChallengeResponse(resp); }; emitterSender?.on(TermEvent.MFA_CHALLENGE, challengeHandler); - return () => { - ssoChallengeAbortController?.abort(); emitterSender?.removeListener(TermEvent.MFA_CHALLENGE, challengeHandler); }; - }, [emitterSender, waitForSsoChallengeResponse]); - - function setErrorText(newErrorText: string) { - setState(prevState => ({ ...prevState, errorText: newErrorText })); - } - - // if any challenge exists, requested is true - const requested = !!( - state.webauthnPublicKey || - state.totpChallenge || - state.ssoChallenge - ); + }, [mfa, emitterSender]); - return { - requested, - onWebauthnAuthenticate, - onSsoAuthenticate, - addMfaToScpUrls: state.addMfaToScpUrls, - setErrorText, - errorText: state.errorText, - webauthnPublicKey: state.webauthnPublicKey, - ssoChallenge: state.ssoChallenge, - }; + return mfa; } export type MfaState = { - onWebauthnAuthenticate: () => void; - onSsoAuthenticate: () => void; - setErrorText: (errorText: string) => void; - errorText: string; - requested: boolean; - addMfaToScpUrls: boolean; - webauthnPublicKey: PublicKeyCredentialRequestOptions; - ssoChallenge: SSOChallenge; + required: boolean; + options: MfaOption[]; + challenge: MfaAuthenticateChallenge; + // Generally you wouldn't pass in a challenge, unless you already + // have one handy, e.g. from a terminal websocket message. + getChallengeResponse: ( + challenge?: MfaAuthenticateChallenge + ) => Promise; + submit: (mfaType?: DeviceType, totpCode?: string) => Promise; + attempt: Attempt; + resetAttempt: () => void; }; // used for testing export function makeDefaultMfaState(): MfaState { return { - onWebauthnAuthenticate: () => null, - onSsoAuthenticate: () => null, - setErrorText: () => null, - errorText: '', - requested: false, - addMfaToScpUrls: false, - webauthnPublicKey: null, - ssoChallenge: null, + required: true, + options: null, + challenge: null, + getChallengeResponse: async () => null, + submit: () => null, + attempt: makeEmptyAttempt(), + resetAttempt: () => null, }; } - -function waitForMessage( - channel: BroadcastChannel, - abortSignal: AbortSignal -): Promise { - return new Promise((resolve, reject) => { - // Create the event listener - function eventHandler(e: MessageEvent) { - // Remove the event listener after it triggers - channel.removeEventListener('message', eventHandler); - // Resolve the promise with the event object - resolve(e); - } - - // Add the event listener - channel.addEventListener('message', eventHandler); - abortSignal.onabort = e => { - channel.removeEventListener('message', eventHandler); - reject(e); - }; - }); -} diff --git a/web/packages/teleport/src/services/api/api.test.ts b/web/packages/teleport/src/services/api/api.test.ts index b9689eeb4210b..af362e602bc4e 100644 --- a/web/packages/teleport/src/services/api/api.test.ts +++ b/web/packages/teleport/src/services/api/api.test.ts @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import { MfaChallengeResponse } from '../mfa'; - import api, { MFA_HEADER, defaultRequestOptions, @@ -28,7 +26,7 @@ import api, { describe('api.fetch', () => { const mockedFetch = jest.spyOn(global, 'fetch').mockResolvedValue({} as any); // we don't care about response - const mfaResp: MfaChallengeResponse = { + const mfaResp = { webauthn_response: { id: 'some-id', type: 'some-type', @@ -104,6 +102,7 @@ describe('api.fetch', () => { ...defaultRequestOptions.headers, ...getAuthHeaders(), [MFA_HEADER]: JSON.stringify({ + ...mfaResp, webauthnAssertionResponse: mfaResp.webauthn_response, }), }, @@ -124,6 +123,7 @@ describe('api.fetch', () => { ...customOpts.headers, ...getAuthHeaders(), [MFA_HEADER]: JSON.stringify({ + ...mfaResp, webauthnAssertionResponse: mfaResp.webauthn_response, }), }, diff --git a/web/packages/teleport/src/services/api/api.ts b/web/packages/teleport/src/services/api/api.ts index 1048c3333e11c..02f1c4ffbb21c 100644 --- a/web/packages/teleport/src/services/api/api.ts +++ b/web/packages/teleport/src/services/api/api.ts @@ -237,8 +237,8 @@ const api = { * If customOptions field is not provided, only fields defined in * `defaultRequestOptions` will be used. * - * @param webauthnResponse if defined (eg: `fetchJsonWithMfaAuthnRetry`) - * will add a custom MFA header field that will hold the webauthn response. + * @param mfaResponse if defined (eg: `fetchJsonWithMfaAuthnRetry`) + * will add a custom MFA header field that will hold the mfaResponse. */ fetch( url: string, @@ -258,7 +258,9 @@ const api = { if (mfaResponse) { options.headers[MFA_HEADER] = JSON.stringify({ - // TODO(Joerger): Handle non-webauthn response. + ...mfaResponse, + // TODO(Joerger): DELETE IN v19.0.0. + // We include webauthnAssertionResponse for backwards compatibility. webauthnAssertionResponse: mfaResponse.webauthn_response, }); } diff --git a/web/packages/teleport/src/services/apps/apps.ts b/web/packages/teleport/src/services/apps/apps.ts index d64f37414a872..268a48915aa2b 100644 --- a/web/packages/teleport/src/services/apps/apps.ts +++ b/web/packages/teleport/src/services/apps/apps.ts @@ -16,11 +16,13 @@ * along with this program. If not, see . */ -import api from 'teleport/services/api'; -import cfg, { UrlAppParams, UrlResourcesParams } from 'teleport/config'; +import cfg, { + CreateAppSessionParams, + UrlAppParams, + UrlResourcesParams, +} from 'teleport/config'; import { ResourcesResponse } from 'teleport/services/agents'; - -import auth, { MfaChallengeScope } from 'teleport/services/auth/auth'; +import api from 'teleport/services/api'; import makeApp from './makeApps'; import { App } from './types'; @@ -41,31 +43,14 @@ const service = { }); }, - async createAppSession(params: UrlAppParams) { - const resolveApp = { - fqdn: params.fqdn, - cluster_name: params.clusterId, - public_addr: params.publicAddr, - }; - - // Prompt for MFA if per-session MFA is required for this app. - const challenge = await auth.getMfaChallenge({ - scope: MfaChallengeScope.USER_SESSION, - allowReuse: false, - isMfaRequiredRequest: { - app: resolveApp, - }, - }); - - const resp = await auth.getMfaChallengeResponse(challenge); - + async createAppSession(params: CreateAppSessionParams) { const createAppSession = { - ...resolveApp, - arn: params.arn, - // TODO(Joerger): Handle non-webauthn response. - mfa_response: resp + ...params, + // TODO(Joerger): DELETE IN v19.0.0. + // We include a string version of the MFA response for backwards compatibility. + mfa_response: params.mfaResponse ? JSON.stringify({ - webauthnAssertionResponse: resp.webauthn_response, + webauthnAssertionResponse: params.mfaResponse.webauthn_response, }) : null, }; diff --git a/web/packages/teleport/src/services/auth/auth.ts b/web/packages/teleport/src/services/auth/auth.ts index 3724f1dc8b056..6480e41931ff2 100644 --- a/web/packages/teleport/src/services/auth/auth.ts +++ b/web/packages/teleport/src/services/auth/auth.ts @@ -23,6 +23,7 @@ import { DeviceUsage, MfaAuthenticateChallenge, MfaChallengeResponse, + SsoChallenge, } from 'teleport/services/mfa'; import { CaptureEvent, userEventService } from 'teleport/services/userEvent'; @@ -289,6 +290,8 @@ const auth = { mfaType = 'totp'; } else if (challenge.webauthnPublicKey) { mfaType = 'webauthn'; + } else if (challenge.ssoChallenge) { + mfaType = 'sso'; } } @@ -296,6 +299,10 @@ const auth = { return auth.getWebAuthnChallengeResponse(challenge.webauthnPublicKey); } + if (mfaType === 'sso') { + return auth.getSsoChallengeResponse(challenge.ssoChallenge); + } + if (mfaType === 'totp') { return { totp_code: totpCode, @@ -333,6 +340,51 @@ const auth = { }); }, + // TODO(Joerger): Delete once no longer used by /e + async getSsoChallengeResponse( + challenge: SsoChallenge + ): Promise { + const abortController = new AbortController(); + + auth.openSsoChallengeRedirect(challenge, abortController); + return await auth.waitForSsoChallengeResponse( + challenge, + abortController.signal + ); + }, + + openSsoChallengeRedirect( + { redirectUrl }: SsoChallenge, + abortController?: AbortController + ) { + // try to center the screen + const width = 1045; + const height = 550; + const left = (screen.width - width) / 2; + const top = (screen.height - height) / 2; + + // these params will open a tiny window. + const params = `width=${width},height=${height},left=${left},top=${top}`; + const w = window.open(redirectUrl, '_blank', params); + + // If the redirect URL window is closed prematurely, abort. + w.onclose = abortController?.abort; + }, + + async waitForSsoChallengeResponse( + { channelId, requestId }: SsoChallenge, + abortSignal: AbortSignal + ): Promise { + const channel = new BroadcastChannel(channelId); + const msg = await waitForMessage(channel, abortSignal); + return { + sso_response: { + requestId, + token: msg.data.mfaToken, + }, + }; + }, + // TODO(Joerger): Delete once no longer used by /e createPrivilegeTokenWithWebauthn() { return auth @@ -430,6 +482,30 @@ function base64EncodeUnicode(str: string) { ); } +function waitForMessage( + channel: BroadcastChannel, + abortSignal: AbortSignal +): Promise { + return new Promise((resolve, reject) => { + // Create the event listener + function eventHandler(e: MessageEvent) { + // Remove the event listener after it triggers + channel.removeEventListener('message', eventHandler); + // Resolve the promise with the event object + resolve(e); + } + + // Add the event listener + channel.addEventListener('message', eventHandler); + + // Close the event listener early if aborted. + abortSignal.onabort = e => { + channel.removeEventListener('message', eventHandler); + reject(e); + }; + }); +} + export default auth; export type IsMfaRequiredRequest = diff --git a/web/packages/teleport/src/services/mfa/mfaOptions.ts b/web/packages/teleport/src/services/mfa/mfaOptions.ts index 96510d31e668f..283feb83eb71f 100644 --- a/web/packages/teleport/src/services/mfa/mfaOptions.ts +++ b/web/packages/teleport/src/services/mfa/mfaOptions.ts @@ -18,7 +18,7 @@ import { Auth2faType } from 'shared/services'; -import { DeviceType, MfaAuthenticateChallenge, SSOChallenge } from './types'; +import { DeviceType, MfaAuthenticateChallenge, SsoChallenge } from './types'; // returns mfa challenge options in order of preferences: WebAuthn > SSO > TOTP. export function getMfaChallengeOptions(mfaChallenge: MfaAuthenticateChallenge) { @@ -74,7 +74,7 @@ export const MFA_OPTION_SSO_DEFAULT: MfaOption = { label: 'SSO', }; -const getSsoMfaOption = (ssoChallenge: SSOChallenge): MfaOption => { +const getSsoMfaOption = (ssoChallenge: SsoChallenge): MfaOption => { return { value: 'sso', label: diff --git a/web/packages/teleport/src/services/mfa/types.ts b/web/packages/teleport/src/services/mfa/types.ts index 382d7831f82fe..f8c0787544d08 100644 --- a/web/packages/teleport/src/services/mfa/types.ts +++ b/web/packages/teleport/src/services/mfa/types.ts @@ -51,7 +51,7 @@ export type SaveNewHardwareDeviceRequest = { }; export type MfaAuthenticateChallengeJson = { - sso_challenge?: SSOChallenge; + sso_challenge?: SsoChallenge; totp_challenge?: boolean; webauthn_challenge?: { publicKey: PublicKeyCredentialRequestOptionsJSON; @@ -59,12 +59,12 @@ export type MfaAuthenticateChallengeJson = { }; export type MfaAuthenticateChallenge = { - ssoChallenge?: SSOChallenge; + ssoChallenge?: SsoChallenge; totpChallenge?: boolean; webauthnPublicKey?: PublicKeyCredentialRequestOptions; }; -export type SSOChallenge = { +export type SsoChallenge = { channelId: string; redirectUrl: string; requestId: string; From dae0e0d478acf0eceee7260518edb2d0428f363e Mon Sep 17 00:00:00 2001 From: Marco Dinis Date: Fri, 20 Dec 2024 09:25:58 +0000 Subject: [PATCH 32/64] AWS OIDC: List Deployed Database Services HTTP API (#49352) * AWS OIDC: List Deployed Database Services HTTP API This PR adds a new endpoint which returns the deployed database services. Calling the ECS APIs requires a region, so we had to iterate over the following resources to collect the relevant regions: - databases - database services - discovery configs * extract loops * improve config parse and moved endpoint to a GET * revert http verb for listing * remove pointer --- .../deployservice_config.go | 23 ++ .../deployservice_config_test.go | 44 +++ lib/web/apiserver.go | 1 + lib/web/integrations_awsoidc.go | 227 ++++++++++++ lib/web/integrations_awsoidc_test.go | 334 ++++++++++++++++++ lib/web/ui/integration.go | 20 ++ 6 files changed, 649 insertions(+) diff --git a/lib/integrations/awsoidc/deployserviceconfig/deployservice_config.go b/lib/integrations/awsoidc/deployserviceconfig/deployservice_config.go index 1f2624b94d6c7..941ba7681f7c0 100644 --- a/lib/integrations/awsoidc/deployserviceconfig/deployservice_config.go +++ b/lib/integrations/awsoidc/deployserviceconfig/deployservice_config.go @@ -89,3 +89,26 @@ func GenerateTeleportConfigString(proxyHostPort, iamTokenName string, resourceMa return teleportConfigString, nil } + +// ParseResourceLabelMatchers receives a teleport config string and returns the Resource Matcher Label. +// The expected input is a base64 encoded yaml string containing a teleport configuration, +// the same format that GenerateTeleportConfigString returns. +func ParseResourceLabelMatchers(teleportConfigStringBase64 string) (types.Labels, error) { + teleportConfigString, err := base64.StdEncoding.DecodeString(teleportConfigStringBase64) + if err != nil { + return nil, trace.BadParameter("invalid base64 value, error=%v", err) + } + + var teleportConfig config.FileConfig + if err := yaml.Unmarshal(teleportConfigString, &teleportConfig); err != nil { + return nil, trace.BadParameter("invalid teleport config, error=%v", err) + } + + if len(teleportConfig.Databases.ResourceMatchers) == 0 { + return nil, trace.BadParameter("valid yaml configuration but db_service.resources has 0 items") + } + + resourceMatchers := teleportConfig.Databases.ResourceMatchers[0] + + return resourceMatchers.Labels, nil +} diff --git a/lib/integrations/awsoidc/deployserviceconfig/deployservice_config_test.go b/lib/integrations/awsoidc/deployserviceconfig/deployservice_config_test.go index 1f47d96e2dac4..3b40912ac9160 100644 --- a/lib/integrations/awsoidc/deployserviceconfig/deployservice_config_test.go +++ b/lib/integrations/awsoidc/deployserviceconfig/deployservice_config_test.go @@ -23,8 +23,10 @@ import ( "testing" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/utils" ) func TestDeployServiceConfig(t *testing.T) { @@ -39,3 +41,45 @@ func TestDeployServiceConfig(t *testing.T) { require.Contains(t, base64Config, base64SeverityDebug) }) } + +func TestParseResourceLabelMatchers(t *testing.T) { + labels := types.Labels{ + "vpc": utils.Strings{"vpc-1", "vpc-2"}, + "region": utils.Strings{"us-west-2"}, + "xyz": utils.Strings{}, + } + base64Config, err := GenerateTeleportConfigString("host:port", "iam-token", labels) + require.NoError(t, err) + + t.Run("recover matching labels", func(t *testing.T) { + gotLabels, err := ParseResourceLabelMatchers(base64Config) + require.NoError(t, err) + + require.Equal(t, labels, gotLabels) + }) + + t.Run("fails if invalid base64 string", func(t *testing.T) { + _, err := ParseResourceLabelMatchers("invalid base 64") + require.ErrorContains(t, err, "base64") + }) + + t.Run("invalid yaml", func(t *testing.T) { + input := base64.StdEncoding.EncodeToString([]byte("invalid yaml")) + _, err := ParseResourceLabelMatchers(input) + require.ErrorContains(t, err, "yaml") + }) + + t.Run("valid yaml but not a teleport config", func(t *testing.T) { + yamlInput := struct { + DBService string `yaml:"db_service"` + }{ + DBService: "not a valid teleport config", + } + yamlBS, err := yaml.Marshal(yamlInput) + require.NoError(t, err) + input := base64.StdEncoding.EncodeToString(yamlBS) + + _, err = ParseResourceLabelMatchers(input) + require.ErrorContains(t, err, "invalid teleport config") + }) +} diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 451f5668a14a2..9a7a0ac625be8 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -996,6 +996,7 @@ func (h *Handler) bindDefaultEndpoints() { h.GET("/webapi/scripts/integrations/configure/listdatabases-iam.sh", h.WithLimiter(h.awsOIDCConfigureListDatabasesIAM)) h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/deployservice", h.WithClusterAuth(h.awsOIDCDeployService)) h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/deploydatabaseservices", h.WithClusterAuth(h.awsOIDCDeployDatabaseServices)) + h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/listdeployeddatabaseservices", h.WithClusterAuth(h.awsOIDCListDeployedDatabaseService)) h.GET("/webapi/scripts/integrations/configure/deployservice-iam.sh", h.WithLimiter(h.awsOIDCConfigureDeployServiceIAM)) h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/ec2", h.WithClusterAuth(h.awsOIDCListEC2)) h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/eksclusters", h.WithClusterAuth(h.awsOIDCListEKSClusters)) diff --git a/lib/web/integrations_awsoidc.go b/lib/web/integrations_awsoidc.go index 44444f191c149..844391b1523f9 100644 --- a/lib/web/integrations_awsoidc.go +++ b/lib/web/integrations_awsoidc.go @@ -23,6 +23,8 @@ import ( "encoding/base64" "encoding/json" "fmt" + "log/slog" + "maps" "net/http" "net/url" "slices" @@ -31,6 +33,7 @@ import ( "github.com/google/safetext/shsprintf" "github.com/gravitational/trace" "github.com/julienschmidt/httprouter" + "google.golang.org/grpc" "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client" @@ -39,6 +42,7 @@ import ( apidefaults "github.com/gravitational/teleport/api/defaults" integrationv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/discoveryconfig" "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/api/utils/aws" "github.com/gravitational/teleport/lib/auth/authclient" @@ -49,6 +53,7 @@ import ( kubeutils "github.com/gravitational/teleport/lib/kube/utils" "github.com/gravitational/teleport/lib/reversetunnelclient" "github.com/gravitational/teleport/lib/services" + libui "github.com/gravitational/teleport/lib/ui" libutils "github.com/gravitational/teleport/lib/utils" awsutils "github.com/gravitational/teleport/lib/utils/aws" "github.com/gravitational/teleport/lib/utils/oidc" @@ -260,6 +265,228 @@ func (h *Handler) awsOIDCDeployDatabaseServices(w http.ResponseWriter, r *http.R }, nil } +// awsOIDCListDeployedDatabaseService lists the deployed Database Services in Amazon ECS. +func (h *Handler) awsOIDCListDeployedDatabaseService(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (any, error) { + ctx := r.Context() + clt, err := sctx.GetUserClient(ctx, site) + if err != nil { + return nil, trace.Wrap(err) + } + + integrationName := p.ByName("name") + if integrationName == "" { + return nil, trace.BadParameter("an integration name is required") + } + + regions, err := fetchRelevantAWSRegions(ctx, clt, clt.DiscoveryConfigClient()) + if err != nil { + return nil, trace.Wrap(err) + } + + services, err := listDeployedDatabaseServices(ctx, h.logger, integrationName, regions, clt.IntegrationAWSOIDCClient()) + if err != nil { + return nil, trace.Wrap(err) + } + + return ui.AWSOIDCListDeployedDatabaseServiceResponse{ + Services: services, + }, nil +} + +type databaseGetter interface { + GetResources(ctx context.Context, req *proto.ListResourcesRequest) (*proto.ListResourcesResponse, error) + GetDatabases(context.Context) ([]types.Database, error) +} + +type discoveryConfigLister interface { + ListDiscoveryConfigs(ctx context.Context, pageSize int, nextToken string) ([]*discoveryconfig.DiscoveryConfig, string, error) +} + +func fetchRelevantAWSRegions(ctx context.Context, authClient databaseGetter, discoveryConfigsClient discoveryConfigLister) ([]string, error) { + regionsSet := make(map[string]struct{}) + + // Collect Regions from Database resources. + databases, err := authClient.GetDatabases(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + for _, resource := range databases { + regionsSet[resource.GetAWS().Region] = struct{}{} + regionsSet[resource.GetAllLabels()[types.DiscoveryLabelRegion]] = struct{}{} + } + + // Iterate over all DatabaseServices and fetch their AWS Region in the matchers. + var nextPageKey string + for { + req := &proto.ListResourcesRequest{ + ResourceType: types.KindDatabaseService, + Limit: defaults.MaxIterationLimit, + StartKey: nextPageKey, + Labels: map[string]string{types.AWSOIDCAgentLabel: types.True}, + } + page, err := client.GetResourcePage[types.DatabaseService](ctx, authClient, req) + if err != nil { + return nil, trace.Wrap(err) + } + + maps.Copy(regionsSet, extractRegionsFromDatabaseServicesPage(page.Resources)) + + if page.NextKey == "" { + break + } + nextPageKey = page.NextKey + } + + // Iterate over all DiscoveryConfigs and fetch their AWS Region in AWS Matchers. + nextPageKey = "" + for { + resp, respNextPageKey, err := discoveryConfigsClient.ListDiscoveryConfigs(ctx, defaults.MaxIterationLimit, nextPageKey) + if err != nil { + return nil, trace.Wrap(err) + } + + maps.Copy(regionsSet, extractRegionsFromDiscoveryConfigPage(resp)) + + if respNextPageKey == "" { + break + } + nextPageKey = respNextPageKey + } + + // Drop any invalid region. + ret := make([]string, 0, len(regionsSet)) + for region := range regionsSet { + if aws.IsValidRegion(region) == nil { + ret = append(ret, region) + } + } + + return ret, nil +} + +func extractRegionsFromDatabaseServicesPage(dbServices []types.DatabaseService) map[string]struct{} { + regionsSet := make(map[string]struct{}) + for _, resource := range dbServices { + for _, matcher := range resource.GetResourceMatchers() { + if matcher.Labels == nil { + continue + } + for labelKey, labelValues := range *matcher.Labels { + if labelKey != types.DiscoveryLabelRegion { + continue + } + for _, labelValue := range labelValues { + regionsSet[labelValue] = struct{}{} + } + } + } + } + + return regionsSet +} + +func extractRegionsFromDiscoveryConfigPage(discoveryConfigs []*discoveryconfig.DiscoveryConfig) map[string]struct{} { + regionsSet := make(map[string]struct{}) + + for _, dc := range discoveryConfigs { + for _, awsMatcher := range dc.Spec.AWS { + for _, region := range awsMatcher.Regions { + regionsSet[region] = struct{}{} + } + } + } + + return regionsSet +} + +type deployedDatabaseServiceLister interface { + ListDeployedDatabaseServices(ctx context.Context, in *integrationv1.ListDeployedDatabaseServicesRequest, opts ...grpc.CallOption) (*integrationv1.ListDeployedDatabaseServicesResponse, error) +} + +func listDeployedDatabaseServices(ctx context.Context, + logger *slog.Logger, + integrationName string, + regions []string, + awsOIDCClient deployedDatabaseServiceLister, +) ([]ui.AWSOIDCDeployedDatabaseService, error) { + var services []ui.AWSOIDCDeployedDatabaseService + for _, region := range regions { + var nextToken string + for { + resp, err := awsOIDCClient.ListDeployedDatabaseServices(ctx, &integrationv1.ListDeployedDatabaseServicesRequest{ + Integration: integrationName, + Region: region, + NextToken: nextToken, + }) + if err != nil { + return nil, trace.Wrap(err) + } + + for _, deployedDatabaseService := range resp.DeployedDatabaseServices { + matchingLabels, err := matchingLabelsFromDeployedService(deployedDatabaseService) + if err != nil { + logger.WarnContext(ctx, "Failed to obtain teleport config string from ECS Service", + "ecs_service", deployedDatabaseService.ServiceDashboardUrl, + "error", err, + ) + } + validTeleportConfigFound := err == nil + + services = append(services, ui.AWSOIDCDeployedDatabaseService{ + Name: deployedDatabaseService.Name, + DashboardURL: deployedDatabaseService.ServiceDashboardUrl, + MatchingLabels: matchingLabels, + ValidTeleportConfig: validTeleportConfigFound, + }) + } + + if resp.NextToken == "" { + break + } + nextToken = resp.NextToken + } + } + return services, nil +} + +func matchingLabelsFromDeployedService(deployedDatabaseService *integrationv1.DeployedDatabaseService) ([]libui.Label, error) { + commandArgs := deployedDatabaseService.ContainerCommand + // This command is what starts the teleport agent in the ECS Service Fargate container. + // See deployservice.go/upsertTask for details. + // It is expected to have at least 3 values, even if dumb-init is removed in the future. + if len(commandArgs) < 3 { + return nil, trace.BadParameter("unexpected command size, expected at least 3 args, got %d", len(commandArgs)) + } + + // The command should have a --config-string flag and then the teleport's base64 encoded configuration as argument + teleportConfigStringFlagIdx := slices.Index(commandArgs, "--config-string") + if teleportConfigStringFlagIdx == -1 { + return nil, trace.BadParameter("missing --config-string flag in container command") + } + if len(commandArgs) < teleportConfigStringFlagIdx+1 { + return nil, trace.BadParameter("missing --config-string argument in container command") + } + teleportConfigString := commandArgs[teleportConfigStringFlagIdx+1] + + labelMatchers, err := deployserviceconfig.ParseResourceLabelMatchers(teleportConfigString) + if err != nil { + return nil, trace.Wrap(err) + } + + var matchingLabels []libui.Label + for labelKey, labelValues := range labelMatchers { + for _, labelValue := range labelValues { + matchingLabels = append(matchingLabels, libui.Label{ + Name: labelKey, + Value: labelValue, + }) + } + } + + return matchingLabels, nil +} + // awsOIDCConfigureDeployServiceIAM returns a script that configures the required IAM permissions to enable the usage of DeployService action. func (h *Handler) awsOIDCConfigureDeployServiceIAM(w http.ResponseWriter, r *http.Request, p httprouter.Params) (any, error) { ctx := r.Context() diff --git a/lib/web/integrations_awsoidc_test.go b/lib/web/integrations_awsoidc_test.go index 9b2660fe36e99..b8414570999dc 100644 --- a/lib/web/integrations_awsoidc_test.go +++ b/lib/web/integrations_awsoidc_test.go @@ -23,6 +23,7 @@ import ( "encoding/base64" "fmt" "net/url" + "strconv" "strings" "testing" @@ -31,13 +32,18 @@ import ( "github.com/google/uuid" "github.com/gravitational/trace" "github.com/stretchr/testify/require" + "google.golang.org/grpc" "github.com/gravitational/teleport/api" "github.com/gravitational/teleport/api/client/proto" integrationv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/discoveryconfig" "github.com/gravitational/teleport/lib/integrations/awsoidc" + "github.com/gravitational/teleport/lib/integrations/awsoidc/deployserviceconfig" "github.com/gravitational/teleport/lib/services" + libui "github.com/gravitational/teleport/lib/ui" + "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/web/ui" ) @@ -1146,3 +1152,331 @@ func TestAWSOIDCAppAccessAppServerCreationDeletion(t *testing.T) { require.NoError(t, err) }) } + +type mockDeployedDatabaseServices struct { + integration string + servicesPerRegion map[string][]*integrationv1.DeployedDatabaseService +} + +func (m *mockDeployedDatabaseServices) ListDeployedDatabaseServices(ctx context.Context, in *integrationv1.ListDeployedDatabaseServicesRequest, opts ...grpc.CallOption) (*integrationv1.ListDeployedDatabaseServicesResponse, error) { + const pageSize = 10 + ret := &integrationv1.ListDeployedDatabaseServicesResponse{} + if in.Integration != m.integration { + return ret, nil + } + + services := m.servicesPerRegion[in.Region] + if len(services) == 0 { + return ret, nil + } + + requestedPage := 1 + totalResources := len(services) + + if in.NextToken != "" { + currentMarker, err := strconv.Atoi(in.NextToken) + if err != nil { + return nil, trace.Wrap(err) + } + requestedPage = currentMarker + } + + sliceStart := pageSize * (requestedPage - 1) + sliceEnd := pageSize * requestedPage + if sliceEnd > totalResources { + sliceEnd = totalResources + } + + ret.DeployedDatabaseServices = services[sliceStart:sliceEnd] + if sliceEnd < totalResources { + ret.NextToken = strconv.Itoa(requestedPage + 1) + } + + return ret, nil +} + +func TestAWSOIDCListDeployedDatabaseServices(t *testing.T) { + ctx := context.Background() + logger := utils.NewSlogLoggerForTests() + + for _, tt := range []struct { + name string + integration string + regions []string + servicesPerRegion func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService + expectedServices func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService + }{ + { + name: "valid", + integration: "my-integration", + regions: []string{"us-west-2"}, + servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService { + command := buildCommandDeployedDatabaseService(t, true, types.Labels{"vpc": []string{"vpc1", "vpc2"}}) + return map[string][]*integrationv1.DeployedDatabaseService{ + "us-west-2": dummyDeployedDatabaseServices(1, command), + } + }, + expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService { + return []ui.AWSOIDCDeployedDatabaseService{{ + Name: "database-service-vpc-0", + DashboardURL: "url", + ValidTeleportConfig: true, + MatchingLabels: []libui.Label{ + {Name: "vpc", Value: "vpc1"}, + {Name: "vpc", Value: "vpc2"}, + }, + }} + }, + }, + { + name: "no regions", + integration: "my-integration", + regions: []string{}, + servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService { + return make(map[string][]*integrationv1.DeployedDatabaseService) + }, + expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService { return nil }, + }, + { + name: "no services", + integration: "my-integration", + regions: []string{"us-west-2"}, + servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService { + return make(map[string][]*integrationv1.DeployedDatabaseService) + }, + expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService { return nil }, + }, + { + name: "services exist but for another region", + integration: "my-integration", + regions: []string{"us-west-2"}, + servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService { + return map[string][]*integrationv1.DeployedDatabaseService{ + "us-west-1": dummyDeployedDatabaseServices(1, []string{}), + } + }, + expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService { return nil }, + }, + { + name: "services exist for multiple regions", + integration: "my-integration", + regions: []string{"us-west-2"}, + servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService { + command := buildCommandDeployedDatabaseService(t, true, types.Labels{"vpc": []string{"vpc1", "vpc2"}}) + return map[string][]*integrationv1.DeployedDatabaseService{ + "us-west-1": dummyDeployedDatabaseServices(1, command), + "us-west-2": dummyDeployedDatabaseServices(1, command), + } + }, + expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService { + return []ui.AWSOIDCDeployedDatabaseService{{ + Name: "database-service-vpc-0", + DashboardURL: "url", + ValidTeleportConfig: true, + MatchingLabels: []libui.Label{ + {Name: "vpc", Value: "vpc1"}, + {Name: "vpc", Value: "vpc2"}, + }, + }} + }, + }, + { + name: "service exist but has invalid configuration", + integration: "my-integration", + regions: []string{"us-west-2"}, + servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService { + command := buildCommandDeployedDatabaseService(t, false, nil) + return map[string][]*integrationv1.DeployedDatabaseService{ + "us-west-2": dummyDeployedDatabaseServices(1, command), + } + }, + expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService { + return []ui.AWSOIDCDeployedDatabaseService{{ + Name: "database-service-vpc-0", + DashboardURL: "url", + ValidTeleportConfig: false, + }} + }, + }, + { + name: "service exist but was changed and --config-string argument is missing", + integration: "my-integration", + regions: []string{"us-west-2"}, + servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService { + command := buildCommandDeployedDatabaseService(t, true, types.Labels{"vpc": []string{"vpc1", "vpc2"}}) + command = command[:len(command)-1] + return map[string][]*integrationv1.DeployedDatabaseService{ + "us-west-2": dummyDeployedDatabaseServices(1, command), + } + }, + expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService { + return []ui.AWSOIDCDeployedDatabaseService{{ + Name: "database-service-vpc-0", + DashboardURL: "url", + ValidTeleportConfig: false, + }} + }, + }, + { + name: "service exist but was changed and --config-string flag is missing", + integration: "my-integration", + regions: []string{"us-west-2"}, + servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService { + command := buildCommandDeployedDatabaseService(t, true, types.Labels{"vpc": []string{"vpc1", "vpc2"}}) + command[1] = "--no-config-string" + return map[string][]*integrationv1.DeployedDatabaseService{ + "us-west-2": dummyDeployedDatabaseServices(1, command), + } + }, + expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService { + return []ui.AWSOIDCDeployedDatabaseService{{ + Name: "database-service-vpc-0", + DashboardURL: "url", + ValidTeleportConfig: false, + }} + }, + }, + { + name: "supports pagination", + integration: "my-integration", + regions: []string{"us-west-2"}, + servicesPerRegion: func(t *testing.T) map[string][]*integrationv1.DeployedDatabaseService { + command := buildCommandDeployedDatabaseService(t, true, types.Labels{"vpc": []string{"vpc1", "vpc2"}}) + return map[string][]*integrationv1.DeployedDatabaseService{ + "us-west-2": dummyDeployedDatabaseServices(1_024, command), + } + }, + expectedServices: func(t *testing.T) []ui.AWSOIDCDeployedDatabaseService { + var ret []ui.AWSOIDCDeployedDatabaseService + for i := 0; i < 1_024; i++ { + ret = append(ret, ui.AWSOIDCDeployedDatabaseService{ + Name: fmt.Sprintf("database-service-vpc-%d", i), + DashboardURL: "url", + ValidTeleportConfig: true, + MatchingLabels: []libui.Label{ + {Name: "vpc", Value: "vpc1"}, + {Name: "vpc", Value: "vpc2"}, + }, + }) + } + return ret + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + clt := &mockDeployedDatabaseServices{ + integration: tt.integration, + servicesPerRegion: tt.servicesPerRegion(t), + } + actual, err := listDeployedDatabaseServices(ctx, logger, tt.integration, tt.regions, clt) + require.NoError(t, err) + expected := tt.expectedServices(t) + require.Equal(t, expected, actual) + }) + } +} + +func buildCommandDeployedDatabaseService(t *testing.T, valid bool, matchingLabels types.Labels) []string { + t.Helper() + if !valid { + return []string{"not valid"} + } + + ret, err := deployserviceconfig.GenerateTeleportConfigString("host", "token", matchingLabels) + require.NoError(t, err) + + return []string{"start", "--config-string", ret} +} + +func dummyDeployedDatabaseServices(count int, command []string) []*integrationv1.DeployedDatabaseService { + var ret []*integrationv1.DeployedDatabaseService + for i := 0; i < count; i++ { + ret = append(ret, &integrationv1.DeployedDatabaseService{ + Name: fmt.Sprintf("database-service-vpc-%d", i), + ServiceDashboardUrl: "url", + ContainerEntryPoint: []string{"teleport"}, + ContainerCommand: command, + }) + } + return ret +} + +func TestFetchRelevantAWSRegions(t *testing.T) { + ctx := context.Background() + + t.Run("resources do not provide any region", func(t *testing.T) { + clt := &mockRelevantAWSRegionsClient{ + databaseServices: &proto.ListResourcesResponse{ + Resources: []*proto.PaginatedResource{}, + }, + databases: make([]types.Database, 0), + discoveryConfigs: make([]*discoveryconfig.DiscoveryConfig, 0), + } + gotRegions, err := fetchRelevantAWSRegions(ctx, clt, clt) + require.NoError(t, err) + require.Empty(t, gotRegions) + }) + + t.Run("resources provide multiple regions", func(t *testing.T) { + clt := &mockRelevantAWSRegionsClient{ + databaseServices: &proto.ListResourcesResponse{ + Resources: []*proto.PaginatedResource{{Resource: &proto.PaginatedResource_DatabaseService{ + DatabaseService: &types.DatabaseServiceV1{Spec: types.DatabaseServiceSpecV1{ + ResourceMatchers: []*types.DatabaseResourceMatcher{ + {Labels: &types.Labels{"region": []string{"us-east-1"}}}, + {Labels: &types.Labels{"region": []string{"us-east-2"}}}, + }, + }}, + }}}, + }, + databases: []types.Database{ + &types.DatabaseV3{Spec: types.DatabaseSpecV3{AWS: types.AWS{Region: "us-west-1"}}}, + &types.DatabaseV3{Metadata: types.Metadata{Labels: map[string]string{"region": "us-west-2"}}}, + }, + discoveryConfigs: []*discoveryconfig.DiscoveryConfig{{ + Spec: discoveryconfig.Spec{AWS: []types.AWSMatcher{{ + Regions: []string{"eu-west-1", "eu-west-2"}, + }}}, + }}, + } + gotRegions, err := fetchRelevantAWSRegions(ctx, clt, clt) + require.NoError(t, err) + expectedRegions := []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "eu-west-1", "eu-west-2"} + require.ElementsMatch(t, expectedRegions, gotRegions) + }) + + t.Run("invalid regions are ignored", func(t *testing.T) { + clt := &mockRelevantAWSRegionsClient{ + databaseServices: &proto.ListResourcesResponse{ + Resources: []*proto.PaginatedResource{}, + }, + databases: []types.Database{ + &types.DatabaseV3{Spec: types.DatabaseSpecV3{AWS: types.AWS{Region: "us-west-1"}}}, + &types.DatabaseV3{Metadata: types.Metadata{Labels: map[string]string{"region": "bad-region"}}}, + }, + discoveryConfigs: make([]*discoveryconfig.DiscoveryConfig, 0), + } + gotRegions, err := fetchRelevantAWSRegions(ctx, clt, clt) + require.NoError(t, err) + expectedRegions := []string{"us-west-1"} + require.ElementsMatch(t, expectedRegions, gotRegions) + }) +} + +type mockRelevantAWSRegionsClient struct { + databaseServices *proto.ListResourcesResponse + databases []types.Database + discoveryConfigs []*discoveryconfig.DiscoveryConfig +} + +func (m *mockRelevantAWSRegionsClient) GetResources(ctx context.Context, req *proto.ListResourcesRequest) (*proto.ListResourcesResponse, error) { + return m.databaseServices, nil +} + +func (m *mockRelevantAWSRegionsClient) GetDatabases(context.Context) ([]types.Database, error) { + return m.databases, nil +} + +func (m *mockRelevantAWSRegionsClient) ListDiscoveryConfigs(ctx context.Context, pageSize int, nextToken string) ([]*discoveryconfig.DiscoveryConfig, string, error) { + return m.discoveryConfigs, "", nil +} diff --git a/lib/web/ui/integration.go b/lib/web/ui/integration.go index b08143978df21..3614a51d09a7f 100644 --- a/lib/web/ui/integration.go +++ b/lib/web/ui/integration.go @@ -371,6 +371,26 @@ type AWSOIDCDeployDatabaseServiceResponse struct { ClusterDashboardURL string `json:"clusterDashboardUrl"` } +// AWSOIDCDeployedDatabaseService represents a Teleport Database Service that is deployed in Amazon ECS. +type AWSOIDCDeployedDatabaseService struct { + // Name is the ECS Service name. + Name string `json:"name,omitempty"` + // DashboardURL is the link to the ECS Service in Amazon Web Console. + DashboardURL string `json:"dashboardUrl,omitempty"` + // ValidTeleportConfig returns whether this ECS Service has a valid Teleport Configuration for a deployed Database Service. + // ECS Services with non-valid configuration require the user to take action on them. + // No MatchingLabels are returned with an invalid configuration. + ValidTeleportConfig bool `json:"validTeleportConfig,omitempty"` + // MatchingLabels are the labels that are used by the Teleport Database Service to know which databases it should proxy. + MatchingLabels []ui.Label `json:"matchingLabels,omitempty"` +} + +// AWSOIDCListDeployedDatabaseServiceResponse is a list of Teleport Database Services that are deployed as ECS Services. +type AWSOIDCListDeployedDatabaseServiceResponse struct { + // Services are the ECS Services. + Services []AWSOIDCDeployedDatabaseService `json:"services"` +} + // AWSOIDCEnrollEKSClustersRequest is a request to ListEKSClusters using the AWS OIDC Integration. type AWSOIDCEnrollEKSClustersRequest struct { // Region is the AWS Region. From d0f95933305bcc17d8e15dd327749b50567e29d7 Mon Sep 17 00:00:00 2001 From: Bartosz Leper Date: Fri, 20 Dec 2024 11:04:57 +0100 Subject: [PATCH 33/64] Move standard editor to its own directory (#50349) --- .../teleport/src/Roles/RoleEditor/RoleEditor.story.tsx | 2 +- .../teleport/src/Roles/RoleEditor/RoleEditor.test.tsx | 2 +- web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx | 4 ++-- .../{ => StandardEditor}/RequiresResetToStandard.tsx | 0 .../RoleEditor/{ => StandardEditor}/StandardEditor.test.tsx | 0 .../RoleEditor/{ => StandardEditor}/StandardEditor.tsx | 6 ++---- .../RoleEditor/{ => StandardEditor}/standardmodel.test.ts | 0 .../Roles/RoleEditor/{ => StandardEditor}/standardmodel.ts | 0 .../src/Roles/RoleEditor/{ => StandardEditor}/validation.ts | 0 .../Roles/RoleEditor/{ => StandardEditor}/withDefaults.ts | 0 10 files changed, 6 insertions(+), 8 deletions(-) rename web/packages/teleport/src/Roles/RoleEditor/{ => StandardEditor}/RequiresResetToStandard.tsx (100%) rename web/packages/teleport/src/Roles/RoleEditor/{ => StandardEditor}/StandardEditor.test.tsx (100%) rename web/packages/teleport/src/Roles/RoleEditor/{ => StandardEditor}/StandardEditor.tsx (99%) rename web/packages/teleport/src/Roles/RoleEditor/{ => StandardEditor}/standardmodel.test.ts (100%) rename web/packages/teleport/src/Roles/RoleEditor/{ => StandardEditor}/standardmodel.ts (100%) rename web/packages/teleport/src/Roles/RoleEditor/{ => StandardEditor}/validation.ts (100%) rename web/packages/teleport/src/Roles/RoleEditor/{ => StandardEditor}/withDefaults.ts (100%) diff --git a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.story.tsx b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.story.tsx index 14efa8fc69588..9fc5ba22f78ce 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.story.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.story.tsx @@ -30,7 +30,7 @@ import { YamlSupportedResourceKind } from 'teleport/services/yaml/types'; import { Access } from 'teleport/services/user'; import useResources from 'teleport/components/useResources'; -import { withDefaults } from './withDefaults'; +import { withDefaults } from './StandardEditor/withDefaults'; import { RoleEditor } from './RoleEditor'; import { RoleEditorDialog } from './RoleEditorDialog'; diff --git a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.test.tsx b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.test.tsx index 0fa38219bd398..aeec6288e2431 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.test.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.test.tsx @@ -31,7 +31,7 @@ import { import { CaptureEvent, userEventService } from 'teleport/services/userEvent'; import { RoleEditor, RoleEditorProps } from './RoleEditor'; -import { defaultOptions, withDefaults } from './withDefaults'; +import { defaultOptions, withDefaults } from './StandardEditor/withDefaults'; // The Ace editor is very difficult to deal with in tests, especially that for // handling its state, we are using input event, which is asynchronous. Thus, diff --git a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx index 729360ecb4254..2eeb3dc9cc9ad 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx @@ -32,11 +32,11 @@ import { newRole, StandardEditorModel, roleToRoleEditorModel as roleToRoleEditorModel, -} from './standardmodel'; +} from './StandardEditor/standardmodel'; import { YamlEditorModel } from './yamlmodel'; import { EditorTab } from './EditorTabs'; import { EditorHeader } from './EditorHeader'; -import { StandardEditor } from './StandardEditor'; +import { StandardEditor } from './StandardEditor/StandardEditor'; import { YamlEditor } from './YamlEditor'; export type RoleEditorProps = { diff --git a/web/packages/teleport/src/Roles/RoleEditor/RequiresResetToStandard.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/RequiresResetToStandard.tsx similarity index 100% rename from web/packages/teleport/src/Roles/RoleEditor/RequiresResetToStandard.tsx rename to web/packages/teleport/src/Roles/RoleEditor/StandardEditor/RequiresResetToStandard.tsx diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/StandardEditor.test.tsx similarity index 100% rename from web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx rename to web/packages/teleport/src/Roles/RoleEditor/StandardEditor/StandardEditor.test.tsx diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/StandardEditor.tsx similarity index 99% rename from web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx rename to web/packages/teleport/src/Roles/RoleEditor/StandardEditor/StandardEditor.tsx index 49c5c5e30feb8..1661eb14974f4 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/StandardEditor.tsx @@ -46,13 +46,12 @@ import { import { SlideTabs } from 'design/SlideTabs'; import { RadioGroup } from 'design/RadioGroup'; import Select from 'shared/components/Select'; - import { components, MultiValueProps } from 'react-select'; - import { Role, RoleWithYaml } from 'teleport/services/resources'; import { LabelsInput } from 'teleport/components/LabelsInput'; +import { FieldMultiInput } from 'shared/components/FieldMultiInput/FieldMultiInput'; -import { FieldMultiInput } from '../../../../shared/components/FieldMultiInput/FieldMultiInput'; +import { EditorSaveCancelButton } from '../Shared'; import { roleEditorModelToRole, @@ -96,7 +95,6 @@ import { WindowsDesktopSpecValidationResult, AccessRuleValidationResult, } from './validation'; -import { EditorSaveCancelButton } from './Shared'; import { RequiresResetToStandard } from './RequiresResetToStandard'; export type StandardEditorProps = { diff --git a/web/packages/teleport/src/Roles/RoleEditor/standardmodel.test.ts b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/standardmodel.test.ts similarity index 100% rename from web/packages/teleport/src/Roles/RoleEditor/standardmodel.test.ts rename to web/packages/teleport/src/Roles/RoleEditor/StandardEditor/standardmodel.test.ts diff --git a/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/standardmodel.ts similarity index 100% rename from web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts rename to web/packages/teleport/src/Roles/RoleEditor/StandardEditor/standardmodel.ts diff --git a/web/packages/teleport/src/Roles/RoleEditor/validation.ts b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/validation.ts similarity index 100% rename from web/packages/teleport/src/Roles/RoleEditor/validation.ts rename to web/packages/teleport/src/Roles/RoleEditor/StandardEditor/validation.ts diff --git a/web/packages/teleport/src/Roles/RoleEditor/withDefaults.ts b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/withDefaults.ts similarity index 100% rename from web/packages/teleport/src/Roles/RoleEditor/withDefaults.ts rename to web/packages/teleport/src/Roles/RoleEditor/StandardEditor/withDefaults.ts From 9450343084420202c4bd67de615bfe49aa029fd2 Mon Sep 17 00:00:00 2001 From: Bartosz Leper Date: Fri, 20 Dec 2024 12:47:59 +0100 Subject: [PATCH 34/64] Split the StandardEditor (#50350) --- .../StandardEditor/AccessRules.test.tsx | 85 ++ .../RoleEditor/StandardEditor/AccessRules.tsx | 145 +++ .../StandardEditor/MetadataSection.tsx | 70 ++ .../RoleEditor/StandardEditor/Options.tsx | 236 +++++ .../StandardEditor/Resources.test.tsx | 465 +++++++++ .../RoleEditor/StandardEditor/Resources.tsx | 486 +++++++++ .../StandardEditor/StandardEditor.test.tsx | 553 +--------- .../StandardEditor/StandardEditor.tsx | 944 +----------------- .../StandardEditor/StatefulSection.tsx | 59 ++ .../RoleEditor/StandardEditor/sections.tsx | 130 +++ 10 files changed, 1688 insertions(+), 1485 deletions(-) create mode 100644 web/packages/teleport/src/Roles/RoleEditor/StandardEditor/AccessRules.test.tsx create mode 100644 web/packages/teleport/src/Roles/RoleEditor/StandardEditor/AccessRules.tsx create mode 100644 web/packages/teleport/src/Roles/RoleEditor/StandardEditor/MetadataSection.tsx create mode 100644 web/packages/teleport/src/Roles/RoleEditor/StandardEditor/Options.tsx create mode 100644 web/packages/teleport/src/Roles/RoleEditor/StandardEditor/Resources.test.tsx create mode 100644 web/packages/teleport/src/Roles/RoleEditor/StandardEditor/Resources.tsx create mode 100644 web/packages/teleport/src/Roles/RoleEditor/StandardEditor/StatefulSection.tsx create mode 100644 web/packages/teleport/src/Roles/RoleEditor/StandardEditor/sections.tsx diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/AccessRules.test.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/AccessRules.test.tsx new file mode 100644 index 0000000000000..1bbd3a5db36c2 --- /dev/null +++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/AccessRules.test.tsx @@ -0,0 +1,85 @@ +/** + * 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 . + */ + +import { render, screen, userEvent } from 'design/utils/testing'; +import { act } from '@testing-library/react'; +import { Validator } from 'shared/components/Validation'; +import selectEvent from 'react-select-event'; +import { ResourceKind } from 'teleport/services/resources'; + +import { RuleModel } from './standardmodel'; +import { AccessRuleValidationResult, validateAccessRule } from './validation'; +import { AccessRules } from './AccessRules'; +import { StatefulSection } from './StatefulSection'; + +describe('AccessRules', () => { + const setup = () => { + const onChange = jest.fn(); + let validator: Validator; + render( + + component={AccessRules} + defaultValue={[]} + onChange={onChange} + validatorRef={v => { + validator = v; + }} + validate={rules => rules.map(validateAccessRule)} + /> + ); + return { user: userEvent.setup(), onChange, validator }; + }; + + test('editing', async () => { + const { user, onChange } = setup(); + await user.click(screen.getByRole('button', { name: 'Add New' })); + await selectEvent.select(screen.getByLabelText('Resources'), [ + 'db', + 'node', + ]); + await selectEvent.select(screen.getByLabelText('Permissions'), [ + 'list', + 'read', + ]); + expect(onChange).toHaveBeenLastCalledWith([ + { + id: expect.any(String), + resources: [ + { label: ResourceKind.Database, value: 'db' }, + { label: ResourceKind.Node, value: 'node' }, + ], + verbs: [ + { label: 'list', value: 'list' }, + { label: 'read', value: 'read' }, + ], + }, + ] as RuleModel[]); + }); + + test('validation', async () => { + const { user, validator } = setup(); + await user.click(screen.getByRole('button', { name: 'Add New' })); + act(() => validator.validate()); + expect( + screen.getByText('At least one resource kind is required') + ).toBeInTheDocument(); + expect( + screen.getByText('At least one permission is required') + ).toBeInTheDocument(); + }); +}); diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/AccessRules.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/AccessRules.tsx new file mode 100644 index 0000000000000..78b680e21e8b3 --- /dev/null +++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/AccessRules.tsx @@ -0,0 +1,145 @@ +/** + * 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 . + */ + +import Flex from 'design/Flex'; + +import { ButtonSecondary } from 'design/Button'; +import { Plus } from 'design/Icon'; +import { + FieldSelect, + FieldSelectCreatable, +} from 'shared/components/FieldSelect'; +import { precomputed } from 'shared/components/Validation/rules'; +import { components, MultiValueProps } from 'react-select'; +import { HoverTooltip } from 'design/Tooltip'; +import styled from 'styled-components'; + +import { AccessRuleValidationResult } from './validation'; +import { + newRuleModel, + ResourceKindOption, + resourceKindOptions, + resourceKindOptionsMap, + RuleModel, + verbOptions, +} from './standardmodel'; +import { Section, SectionProps } from './sections'; + +export function AccessRules({ + value, + isProcessing, + validation, + onChange, +}: SectionProps) { + function addRule() { + onChange?.([...value, newRuleModel()]); + } + function setRule(rule: RuleModel) { + onChange?.(value.map(r => (r.id === rule.id ? rule : r))); + } + function removeRule(id: string) { + onChange?.(value.filter(r => r.id !== id)); + } + return ( + + {value.map((rule, i) => ( + removeRule(rule.id)} + /> + ))} + + + Add New + + + ); +} + +function AccessRule({ + value, + isProcessing, + validation, + onChange, + onRemove, +}: SectionProps & { + onRemove?(): void; +}) { + const { resources, verbs } = value; + return ( +
    + onChange?.({ ...value, resources: r })} + rule={precomputed(validation.fields.resources)} + /> + onChange?.({ ...value, verbs: v })} + rule={precomputed(validation.fields.verbs)} + mb={0} + /> +
    + ); +} + +const ResourceKindSelect = styled( + FieldSelectCreatable +)` + .teleport-resourcekind__value--unknown { + background: ${props => props.theme.colors.interactive.solid.alert.default}; + .react-select__multi-value__label, + .react-select__multi-value__remove { + color: ${props => props.theme.colors.text.primaryInverse}; + } + } +`; + +function ResourceKindMultiValue(props: MultiValueProps) { + if (resourceKindOptionsMap.has(props.data.value)) { + return ; + } + return ( + + + + ); +} diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/MetadataSection.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/MetadataSection.tsx new file mode 100644 index 0000000000000..605a101964e5e --- /dev/null +++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/MetadataSection.tsx @@ -0,0 +1,70 @@ +/** + * 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 . + */ + +import FieldInput from 'shared/components/FieldInput'; + +import { precomputed } from 'shared/components/Validation/rules'; + +import { LabelsInput } from 'teleport/components/LabelsInput'; + +import Text from 'design/Text'; + +import { Section, SectionProps } from './sections'; +import { MetadataModel } from './standardmodel'; +import { MetadataValidationResult } from './validation'; + +export const MetadataSection = ({ + value, + isProcessing, + validation, + onChange, +}: SectionProps) => ( +
    + onChange({ ...value, name: e.target.value })} + /> + ) => + onChange({ ...value, description: e.target.value }) + } + /> + + Labels + + onChange?.({ ...value, labels })} + rule={precomputed(validation.fields.labels)} + /> +
    +); diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/Options.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/Options.tsx new file mode 100644 index 0000000000000..cf510a5050aad --- /dev/null +++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor/Options.tsx @@ -0,0 +1,236 @@ +/** + * 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 . + */ + +import Box from 'design/Box'; +import Input from 'design/Input'; +import LabelInput from 'design/LabelInput'; +import { RadioGroup } from 'design/RadioGroup'; +import { H4 } from 'design/Text'; +import { useId } from 'react'; +import styled, { useTheme } from 'styled-components'; + +import Select from 'shared/components/Select'; + +import { SectionProps } from './sections'; +import { + OptionsModel, + requireMFATypeOptions, + sessionRecordingModeOptions, + createHostUserModeOptions, + createDBUserModeOptions, +} from './standardmodel'; + +export function Options({ + value, + isProcessing, + onChange, +}: SectionProps) { + const theme = useTheme(); + const id = useId(); + const maxSessionTTLId = `${id}-max-session-ttl`; + const clientIdleTimeoutId = `${id}-client-idle-timeout`; + const requireMFATypeId = `${id}-require-mfa-type`; + const createHostUserModeId = `${id}-create-host-user-mode`; + const createDBUserModeId = `${id}-create-db-user-mode`; + const defaultSessionRecordingModeId = `${id}-default-session-recording-mode`; + const sshSessionRecordingModeId = `${id}-ssh-session-recording-mode`; + return ( + + Global Settings + + Max Session TTL + onChange({ ...value, maxSessionTTL: e.target.value })} + /> + + + Client Idle Timeout + + + onChange({ ...value, clientIdleTimeout: e.target.value }) + } + /> + + Disconnect When Certificate Expires + onChange({ ...value, disconnectExpiredCert: d })} + /> + + Require Session MFA + onChange?.({ ...value, defaultSessionRecordingMode: m })} + /> + + SSH + + + Create Host User Mode + + onChange?.({ ...value, sshSessionRecordingMode: m })} + /> + + Database + + Create Database User + onChange({ ...value, createDBUser: c })} + /> + + {/* TODO(bl-nero): a bug in YAML unmarshalling backend breaks the + createDBUserMode field. Fix it and add the field here. */} + + Create Database User Mode + + onChange({ ...value, maxSessionTTL: e.target.value })} - /> - - - Client Idle Timeout - - - onChange({ ...value, clientIdleTimeout: e.target.value }) - } - /> - - Disconnect When Certificate Expires - onChange({ ...value, disconnectExpiredCert: d })} - /> - - Require Session MFA - onChange?.({ ...value, defaultSessionRecordingMode: m })} - /> - - SSH - - - Create Host User Mode - - onChange?.({ ...value, sshSessionRecordingMode: m })} - /> - - Database - - Create Database User - onChange({ ...value, createDBUser: c })} - /> - - {/* TODO(bl-nero): a bug in YAML unmarshalling backend breaks the - createDBUserMode field. Fix it and add the field here. */} - - Create Database User Mode - -