diff --git a/lib/integrations/awsoidc/create_ec2ice.go b/lib/integrations/awsoidc/create_ec2ice.go index d0bd9a5072b..4d63b78b53a 100644 --- a/lib/integrations/awsoidc/create_ec2ice.go +++ b/lib/integrations/awsoidc/create_ec2ice.go @@ -26,6 +26,8 @@ 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" + + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" ) // CreateEC2ICERequest contains the required fields to create an AWS EC2 Instance Connect Endpoint. @@ -46,7 +48,7 @@ type CreateEC2ICERequest struct { // - teleport.dev/cluster: // - teleport.dev/origin: aws-oidc-integration // - teleport.dev/integration: - ResourceCreationTags AWSTags + ResourceCreationTags tags.AWSTags } // EC2ICEEndpoint contains the information for a single Endpoint to be created. @@ -83,7 +85,7 @@ func (req *CreateEC2ICERequest) CheckAndSetDefaults() error { } if len(req.ResourceCreationTags) == 0 { - req.ResourceCreationTags = defaultResourceCreationTags(req.Cluster, req.IntegrationName) + req.ResourceCreationTags = tags.DefaultResourceCreationTags(req.Cluster, req.IntegrationName) } return nil diff --git a/lib/integrations/awsoidc/create_ec2ice_test.go b/lib/integrations/awsoidc/create_ec2ice_test.go index da71be0ec1a..688f16ec213 100644 --- a/lib/integrations/awsoidc/create_ec2ice_test.go +++ b/lib/integrations/awsoidc/create_ec2ice_test.go @@ -29,6 +29,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/gravitational/trace" "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" ) type mockCreateEC2ICEClient struct { @@ -185,7 +187,7 @@ func TestCreateEC2ICERequest(t *testing.T) { SubnetID: "subnet-123", SecurityGroupIDs: []string{"sg-1", "sg-2"}, }}, - ResourceCreationTags: AWSTags{ + ResourceCreationTags: tags.AWSTags{ "teleport.dev/origin": "integration_awsoidc", "teleport.dev/cluster": "teleport-cluster", "teleport.dev/integration": "teleportdev", diff --git a/lib/integrations/awsoidc/deploydatabaseservice.go b/lib/integrations/awsoidc/deploydatabaseservice.go index f339a2978c9..f17552d62c7 100644 --- a/lib/integrations/awsoidc/deploydatabaseservice.go +++ b/lib/integrations/awsoidc/deploydatabaseservice.go @@ -27,6 +27,7 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" ) // DeployDatabaseServiceRequest contains the required fields to deploy multiple Teleport Databases Services. @@ -58,7 +59,7 @@ type DeployDatabaseServiceRequest struct { TeleportVersionTag string // ResourceCreationTags is used to add tags when creating resources in AWS. - ResourceCreationTags AWSTags + ResourceCreationTags tags.AWSTags // DeploymentJoinTokenName is the Teleport IAM Join Token name that the deployed service must use to join the cluster. DeploymentJoinTokenName string @@ -115,7 +116,7 @@ func (r *DeployDatabaseServiceRequest) CheckAndSetDefaults() error { } if r.ResourceCreationTags == nil { - r.ResourceCreationTags = defaultResourceCreationTags(r.TeleportClusterName, r.IntegrationName) + r.ResourceCreationTags = tags.DefaultResourceCreationTags(r.TeleportClusterName, r.IntegrationName) } r.ecsClusterName = normalizeECSClusterName(r.TeleportClusterName) diff --git a/lib/integrations/awsoidc/deploydatabaseservice_test.go b/lib/integrations/awsoidc/deploydatabaseservice_test.go index 48ad7ce8302..a61309f9c83 100644 --- a/lib/integrations/awsoidc/deploydatabaseservice_test.go +++ b/lib/integrations/awsoidc/deploydatabaseservice_test.go @@ -39,6 +39,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" ) func TestDeployDatabaseServiceRequest_CheckAndSetDefaults(t *testing.T) { @@ -157,7 +158,7 @@ func TestDeployDatabaseServiceRequest_CheckAndSetDefaults(t *testing.T) { Region: "r", TaskRoleARN: "arn", IntegrationName: "teleportdev", - ResourceCreationTags: AWSTags{ + ResourceCreationTags: tags.AWSTags{ "teleport.dev/origin": "integration_awsoidc", "teleport.dev/cluster": "mycluster", "teleport.dev/integration": "teleportdev", @@ -202,7 +203,7 @@ type mockDeployServiceClient struct { iamTokenMissing bool iamAccessDeniedListServices bool - defaultTags AWSTags + defaultTags tags.AWSTags } // DescribeClusters lists ECS Clusters. diff --git a/lib/integrations/awsoidc/deployservice.go b/lib/integrations/awsoidc/deployservice.go index 0906b056ee7..4fe563d47ff 100644 --- a/lib/integrations/awsoidc/deployservice.go +++ b/lib/integrations/awsoidc/deployservice.go @@ -35,6 +35,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/retryutils" + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" "github.com/gravitational/teleport/lib/modules" ) @@ -141,7 +142,7 @@ type DeployServiceRequest struct { IntegrationName string // ResourceCreationTags is used to add tags when creating resources in AWS. - ResourceCreationTags AWSTags + ResourceCreationTags tags.AWSTags // DeploymentMode is the identifier of a deployment mode - which Teleport Services to enable and their configuration. DeploymentMode string @@ -250,7 +251,7 @@ func (r *DeployServiceRequest) CheckAndSetDefaults() error { } if r.ResourceCreationTags == nil { - r.ResourceCreationTags = defaultResourceCreationTags(r.TeleportClusterName, r.IntegrationName) + r.ResourceCreationTags = tags.DefaultResourceCreationTags(r.TeleportClusterName, r.IntegrationName) } if r.TeleportConfigString == "" { @@ -471,7 +472,7 @@ type upsertTaskRequest struct { ClusterName string ServiceName string TeleportVersionTag string - ResourceCreationTags AWSTags + ResourceCreationTags tags.AWSTags Region string TeleportConfigB64 string } @@ -542,7 +543,7 @@ func upsertTask(ctx context.Context, clt DeployServiceClient, req upsertTaskRequ // It will re-create if its status is INACTIVE. // If the cluster status is not ACTIVE, an error is returned. // The cluster is returned. -func upsertCluster(ctx context.Context, clt DeployServiceClient, clusterName string, resourceCreationTags AWSTags) (*ecsTypes.Cluster, error) { +func upsertCluster(ctx context.Context, clt DeployServiceClient, clusterName string, resourceCreationTags tags.AWSTags) (*ecsTypes.Cluster, error) { describeClustersResponse, err := clt.DescribeClusters(ctx, &ecs.DescribeClustersInput{ Clusters: []string{clusterName}, Include: []ecsTypes.ClusterField{ @@ -687,7 +688,7 @@ func deployServiceNetworkConfiguration(subnetIDs, securityGroups []string) *ecsT type upsertServiceRequest struct { ServiceName string ClusterName string - ResourceCreationTags AWSTags + ResourceCreationTags tags.AWSTags SubnetIDs []string SecurityGroups []string } diff --git a/lib/integrations/awsoidc/deployservice_iam_config.go b/lib/integrations/awsoidc/deployservice_iam_config.go index 82b78958999..0420a7b7d90 100644 --- a/lib/integrations/awsoidc/deployservice_iam_config.go +++ b/lib/integrations/awsoidc/deployservice_iam_config.go @@ -30,6 +30,7 @@ import ( awsapiutils "github.com/gravitational/teleport/api/utils/aws" awslib "github.com/gravitational/teleport/lib/cloud/aws" + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" awslibutils "github.com/gravitational/teleport/lib/utils/aws" ) @@ -68,7 +69,7 @@ type DeployServiceIAMConfigureRequest struct { // - teleport.dev/cluster: // - teleport.dev/origin: aws-oidc-integration // - teleport.dev/integration: - ResourceCreationTags AWSTags + ResourceCreationTags tags.AWSTags // partitionID is the AWS Partition ID. // Eg, aws, aws-cn, aws-us-gov @@ -103,7 +104,7 @@ func (r *DeployServiceIAMConfigureRequest) CheckAndSetDefaults() error { } if len(r.ResourceCreationTags) == 0 { - r.ResourceCreationTags = defaultResourceCreationTags(r.Cluster, r.IntegrationName) + r.ResourceCreationTags = tags.DefaultResourceCreationTags(r.Cluster, r.IntegrationName) } r.partitionID = awsapiutils.GetPartitionFromRegion(r.Region) diff --git a/lib/integrations/awsoidc/deployservice_iam_config_test.go b/lib/integrations/awsoidc/deployservice_iam_config_test.go index fd04ac4895b..ad87ff4c6b2 100644 --- a/lib/integrations/awsoidc/deployservice_iam_config_test.go +++ b/lib/integrations/awsoidc/deployservice_iam_config_test.go @@ -29,6 +29,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/gravitational/trace" "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" ) var badParameterCheck = func(t require.TestingT, err error, msgAndArgs ...interface{}) { @@ -72,7 +74,7 @@ func TestDeployServiceIAMConfigReqDefaults(t *testing.T) { TaskRole: "taskrole", partitionID: "aws", IntegrationRoleDeployServicePolicy: "DeployService", - ResourceCreationTags: AWSTags{ + ResourceCreationTags: tags.AWSTags{ "teleport.dev/cluster": "mycluster", "teleport.dev/integration": "myintegration", "teleport.dev/origin": "integration_awsoidc", diff --git a/lib/integrations/awsoidc/deployservice_test.go b/lib/integrations/awsoidc/deployservice_test.go index 5dbe9081736..10d62e7da5b 100644 --- a/lib/integrations/awsoidc/deployservice_test.go +++ b/lib/integrations/awsoidc/deployservice_test.go @@ -32,6 +32,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/automaticupgrades" + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" ) func TestDeployServiceRequest(t *testing.T) { @@ -152,7 +153,7 @@ func TestDeployServiceRequest(t *testing.T) { TaskName: stringPointer("mycluster-teleport-database-service"), DeploymentJoinTokenName: "discover-aws-oidc-iam-token", IntegrationName: "teleportdev", - ResourceCreationTags: AWSTags{ + ResourceCreationTags: tags.AWSTags{ "teleport.dev/origin": "integration_awsoidc", "teleport.dev/cluster": "mycluster", "teleport.dev/integration": "teleportdev", diff --git a/lib/integrations/awsoidc/deployservice_update.go b/lib/integrations/awsoidc/deployservice_update.go index 3c125326e79..a23e6c6c082 100644 --- a/lib/integrations/awsoidc/deployservice_update.go +++ b/lib/integrations/awsoidc/deployservice_update.go @@ -32,6 +32,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/automaticupgrades" awslib "github.com/gravitational/teleport/lib/cloud/aws" + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" ) // waitDuration specifies the amount of time to wait for a service to become healthy after an update. @@ -44,7 +45,7 @@ type UpdateServiceRequest struct { // TeleportVersionTag specifies the desired teleport version in the format "13.4.0" TeleportVersionTag string // OwnershipTags specifies ownership tags - OwnershipTags AWSTags + OwnershipTags tags.AWSTags } // CheckAndSetDefaults checks and sets default config values. @@ -90,7 +91,7 @@ func UpdateDeployService(ctx context.Context, clt DeployServiceClient, log *slog return nil } -func updateServiceContainerImage(ctx context.Context, clt DeployServiceClient, log *slog.Logger, service *ecsTypes.Service, teleportImage string, ownershipTags AWSTags) error { +func updateServiceContainerImage(ctx context.Context, clt DeployServiceClient, log *slog.Logger, service *ecsTypes.Service, teleportImage string, ownershipTags tags.AWSTags) error { taskDefinition, err := getManagedTaskDefinition(ctx, clt, aws.ToString(service.TaskDefinition), ownershipTags) if err != nil { return trace.Wrap(err) @@ -166,7 +167,7 @@ func getAllServiceNamesForCluster(ctx context.Context, clt DeployServiceClient, return ret, nil } -func getManagedServices(ctx context.Context, clt DeployServiceClient, log *slog.Logger, teleportClusterName string, ownershipTags AWSTags) ([]ecsTypes.Service, error) { +func getManagedServices(ctx context.Context, clt DeployServiceClient, log *slog.Logger, teleportClusterName string, ownershipTags tags.AWSTags) ([]ecsTypes.Service, error) { // The Cluster name is created using the Teleport Cluster Name. // Check the DeployDatabaseServiceRequest.CheckAndSetDefaults // and DeployServiceRequest.CheckAndSetDefaults. @@ -224,7 +225,7 @@ func getManagedServices(ctx context.Context, clt DeployServiceClient, log *slog. return ecsServices, nil } -func getManagedTaskDefinition(ctx context.Context, clt DeployServiceClient, taskDefinitionName string, ownershipTags AWSTags) (*ecsTypes.TaskDefinition, error) { +func getManagedTaskDefinition(ctx context.Context, clt DeployServiceClient, taskDefinitionName string, ownershipTags tags.AWSTags) (*ecsTypes.TaskDefinition, error) { describeTaskDefinitionOut, err := clt.DescribeTaskDefinition(ctx, &ecs.DescribeTaskDefinitionInput{ TaskDefinition: aws.String(taskDefinitionName), Include: []ecsTypes.TaskDefinitionField{ecsTypes.TaskDefinitionFieldTags}, diff --git a/lib/integrations/awsoidc/deployservice_update_test.go b/lib/integrations/awsoidc/deployservice_update_test.go index b297d73febc..91bd4ada8ae 100644 --- a/lib/integrations/awsoidc/deployservice_update_test.go +++ b/lib/integrations/awsoidc/deployservice_update_test.go @@ -30,6 +30,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/automaticupgrades" + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" "github.com/gravitational/teleport/lib/utils" ) @@ -160,7 +161,7 @@ func TestUpdateDeployServices(t *testing.T) { clusterName := "my-cluster" integrationName := "my-integration" - ownershipTags := defaultResourceCreationTags(clusterName, integrationName) + ownershipTags := tags.DefaultResourceCreationTags(clusterName, integrationName) teleportVersion := teleport.Version log := utils.NewSlogLoggerForTests().With("test", t.Name()) diff --git a/lib/integrations/awsoidc/idp_iam_config.go b/lib/integrations/awsoidc/idp_iam_config.go index aba4ed472dc..3b22490029b 100644 --- a/lib/integrations/awsoidc/idp_iam_config.go +++ b/lib/integrations/awsoidc/idp_iam_config.go @@ -41,6 +41,7 @@ import ( "github.com/gravitational/teleport/api/types" awslib "github.com/gravitational/teleport/lib/cloud/aws" "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" awsutil "github.com/gravitational/teleport/lib/utils/aws" "github.com/gravitational/teleport/lib/utils/oidc" ) @@ -98,7 +99,7 @@ type IdPIAMConfigureRequest struct { // IntegrationRole is the Integration's AWS Role used to set up Teleport as an OIDC IdP. IntegrationRole string - ownershipTags AWSTags + ownershipTags tags.AWSTags } // CheckAndSetDefaults ensures the required fields are present. @@ -151,7 +152,7 @@ func (r *IdPIAMConfigureRequest) CheckAndSetDefaults() error { } } - r.ownershipTags = defaultResourceCreationTags(r.Cluster, r.IntegrationName) + r.ownershipTags = tags.DefaultResourceCreationTags(r.Cluster, r.IntegrationName) return nil } diff --git a/lib/integrations/awsoidc/idp_iam_config_test.go b/lib/integrations/awsoidc/idp_iam_config_test.go index a24c635e81c..9b5b10d9bd7 100644 --- a/lib/integrations/awsoidc/idp_iam_config_test.go +++ b/lib/integrations/awsoidc/idp_iam_config_test.go @@ -38,6 +38,7 @@ import ( "github.com/stretchr/testify/require" "github.com/gravitational/teleport/lib" + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" ) func TestIdPIAMConfigReqDefaults(t *testing.T) { @@ -79,7 +80,7 @@ func TestIdPIAMConfigReqDefaults(t *testing.T) { ProxyPublicAddress: "https://proxy.example.com", issuer: "proxy.example.com", issuerURL: "https://proxy.example.com", - ownershipTags: AWSTags{ + ownershipTags: tags.AWSTags{ "teleport.dev/cluster": "mycluster", "teleport.dev/integration": "myintegration", "teleport.dev/origin": "integration_awsoidc", @@ -110,7 +111,7 @@ func TestIdPIAMConfigReqDefaults(t *testing.T) { S3JWKSContentsB64: base64EncodedString, issuer: "bucket-1.s3.amazonaws.com/prefix-2", issuerURL: "https://bucket-1.s3.amazonaws.com/prefix-2", - ownershipTags: AWSTags{ + ownershipTags: tags.AWSTags{ "teleport.dev/cluster": "mycluster", "teleport.dev/integration": "myintegration", "teleport.dev/origin": "integration_awsoidc", diff --git a/lib/integrations/awsoidc/tags.go b/lib/integrations/awsoidc/tags/tags.go similarity index 76% rename from lib/integrations/awsoidc/tags.go rename to lib/integrations/awsoidc/tags/tags.go index 7110df2b18d..0ed290105a1 100644 --- a/lib/integrations/awsoidc/tags.go +++ b/lib/integrations/awsoidc/tags/tags.go @@ -16,15 +16,18 @@ * along with this program. If not, see . */ -package awsoidc +package tags import ( "fmt" + "maps" "strings" + athenatypes "github.com/aws/aws-sdk-go-v2/service/athena/types" ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types" ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/gravitational/teleport/api/types" ) @@ -41,12 +44,12 @@ func (d AWSTags) String() string { return strings.Join(tagsString, ", ") } -// defaultResourceCreationTags returns the default tags that should be applied when creating new AWS resources. +// DefaultResourceCreationTags returns the default tags that should be applied when creating new AWS resources. // The following tags are returned: // - teleport.dev/cluster: // - teleport.dev/origin: aws-oidc-integration // - teleport.dev/integration: -func defaultResourceCreationTags(clusterName, integrationName string) AWSTags { +func DefaultResourceCreationTags(clusterName, integrationName string) AWSTags { return AWSTags{ types.ClusterLabel: clusterName, types.OriginLabel: types.OriginIntegrationAWSOIDC, @@ -58,7 +61,6 @@ func defaultResourceCreationTags(clusterName, integrationName string) AWSTags { func (d AWSTags) ToECSTags() []ecsTypes.Tag { ecsTags := make([]ecsTypes.Tag, 0, len(d)) for k, v := range d { - k, v := k, v ecsTags = append(ecsTags, ecsTypes.Tag{ Key: &k, Value: &v, @@ -71,7 +73,6 @@ func (d AWSTags) ToECSTags() []ecsTypes.Tag { func (d AWSTags) ToEC2Tags() []ec2Types.Tag { ec2Tags := make([]ec2Types.Tag, 0, len(d)) for k, v := range d { - k, v := k, v ec2Tags = append(ec2Tags, ec2Types.Tag{ Key: &k, Value: &v, @@ -118,7 +119,6 @@ func (d AWSTags) MatchesIAMTags(resourceTags []iamTypes.Tag) bool { func (d AWSTags) ToIAMTags() []iamTypes.Tag { iamTags := make([]iamTypes.Tag, 0, len(d)) for k, v := range d { - k, v := k, v iamTags = append(iamTags, iamTypes.Tag{ Key: &k, Value: &v, @@ -126,3 +126,33 @@ func (d AWSTags) ToIAMTags() []iamTypes.Tag { } return iamTags } + +// ToS3Tags returns the default tags using the expected type for S3 resources: [s3types.Tag] +func (d AWSTags) ToS3Tags() []s3types.Tag { + s3Tags := make([]s3types.Tag, 0, len(d)) + for k, v := range d { + s3Tags = append(s3Tags, s3types.Tag{ + Key: &k, + Value: &v, + }) + } + return s3Tags +} + +// ToAthenaTags returns the default tags using the expected type for Athena resources: [athenatypes.Tag] +func (d AWSTags) ToAthenaTags() []athenatypes.Tag { + athenaTags := make([]athenatypes.Tag, 0, len(d)) + for k, v := range d { + athenaTags = append(athenaTags, athenatypes.Tag{ + Key: &k, + Value: &v, + }) + } + return athenaTags +} + +// ToMap returns the default tags using the expected type for other aws resources. +// Eg Glue resources +func (d AWSTags) ToMap() map[string]string { + return maps.Clone((map[string]string)(d)) +} diff --git a/lib/integrations/awsoidc/tags_test.go b/lib/integrations/awsoidc/tags/tags_test.go similarity index 98% rename from lib/integrations/awsoidc/tags_test.go rename to lib/integrations/awsoidc/tags/tags_test.go index 899235600ff..7b07e86f934 100644 --- a/lib/integrations/awsoidc/tags_test.go +++ b/lib/integrations/awsoidc/tags/tags_test.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package awsoidc +package tags import ( "testing" @@ -31,7 +31,7 @@ import ( func TestDefaultTags(t *testing.T) { clusterName := "mycluster" integrationName := "myawsaccount" - d := defaultResourceCreationTags(clusterName, integrationName) + d := DefaultResourceCreationTags(clusterName, integrationName) expectedTags := AWSTags{ "teleport.dev/cluster": "mycluster", diff --git a/lib/integrations/externalauditstorage/bootstrap.go b/lib/integrations/externalauditstorage/bootstrap.go index 62e1cc55bbe..9181b050886 100644 --- a/lib/integrations/externalauditstorage/bootstrap.go +++ b/lib/integrations/externalauditstorage/bootstrap.go @@ -35,6 +35,7 @@ import ( "github.com/gravitational/trace" eastypes "github.com/gravitational/teleport/api/types/externalauditstorage" + "github.com/gravitational/teleport/lib/integrations/awsoidc/tags" awsutil "github.com/gravitational/teleport/lib/utils/aws" ) @@ -53,6 +54,9 @@ type BootstrapInfraParams struct { Spec *eastypes.ExternalAuditStorageSpec Region string + + ClusterName string + IntegrationName string } // BootstrapAthenaClient is a subset of [athena.Client] methods needed for athena bootstrap. @@ -81,6 +85,8 @@ type BootstrapS3Client interface { PutBucketVersioning(ctx context.Context, params *s3.PutBucketVersioningInput, optFns ...func(*s3.Options)) (*s3.PutBucketVersioningOutput, error) // Creates a new lifecycle configuration for the bucket or replaces an existing lifecycle configuration. PutBucketLifecycleConfiguration(ctx context.Context, params *s3.PutBucketLifecycleConfigurationInput, optFns ...func(*s3.Options)) (*s3.PutBucketLifecycleConfigurationOutput, error) + // Adds tags to a bucket. + PutBucketTagging(ctx context.Context, params *s3.PutBucketTaggingInput, optFns ...func(*s3.Options)) (*s3.PutBucketTaggingOutput, error) } // BootstrapInfra bootstraps External Audit Storage infrastructure. @@ -98,6 +104,10 @@ func BootstrapInfra(ctx context.Context, params BootstrapInfraParams) error { return trace.BadParameter("param S3 required") case params.Region == "": return trace.BadParameter("param Region required") + case params.ClusterName == "": + return trace.BadParameter("param Cluster Name required") + case params.IntegrationName == "": + return trace.BadParameter("param Integration Name required") case params.Spec == nil: return trace.BadParameter("param Spec required") } @@ -107,19 +117,23 @@ func BootstrapInfra(ctx context.Context, params BootstrapInfraParams) error { return trace.Wrap(err) } - if err := createLTSBucket(ctx, params.S3, ltsBucket, params.Region); err != nil { + ownershipTags := tags.DefaultResourceCreationTags(params.ClusterName, params.IntegrationName) + s3OwnershipTags := ownershipTags.ToS3Tags() + + if err := createLTSBucket(ctx, params.S3, ltsBucket, params.Region, s3OwnershipTags); err != nil { return trace.Wrap(err) } - if err := createTransientBucket(ctx, params.S3, transientBucket, params.Region); err != nil { + if err := createTransientBucket(ctx, params.S3, transientBucket, params.Region, s3OwnershipTags); err != nil { return trace.Wrap(err) } - if err := createAthenaWorkgroup(ctx, params.Athena, params.Spec.AthenaWorkgroup); err != nil { + athenaOwnershipTags := ownershipTags.ToAthenaTags() + if err := createAthenaWorkgroup(ctx, params.Athena, params.Spec.AthenaWorkgroup, athenaOwnershipTags); err != nil { return trace.Wrap(err) } - if err := createGlueInfra(ctx, params.Glue, params.Spec.GlueTable, params.Spec.GlueDatabase, ltsBucket); err != nil { + if err := createGlueInfra(ctx, params.Glue, params.Region, params.Spec.GlueTable, params.Spec.GlueDatabase, ltsBucket, ownershipTags.ToMap()); err != nil { return trace.Wrap(err) } @@ -132,11 +146,15 @@ func BootstrapInfra(ctx context.Context, params BootstrapInfraParams) error { // * Object locking enabled with Governance mode and default retention of 4 years // * Object ownership set to BucketOwnerEnforced // * Default SSE-S3 encryption -func createLTSBucket(ctx context.Context, clt BootstrapS3Client, bucketName string, region string) error { +func createLTSBucket(ctx context.Context, clt BootstrapS3Client, bucketName string, region string, ownershipTags []s3types.Tag) error { fmt.Printf("Creating long term storage S3 bucket %s\n", bucketName) - err := createBucket(ctx, clt, bucketName, region, true) - if err != nil && !trace.IsAlreadyExists(err) { - return trace.Wrap(err, "creating long term storage S3 bucket") + err := ensureBucket(ctx, clt, bucketName, region, true) + if err != nil { + return trace.Wrap(err, "creating transient S3 bucket") + } + + if err := applyBucketProperties(ctx, clt, bucketName, ownershipTags); err != nil { + return trace.Wrap(err, "applying S3 bucket properties") } fmt.Printf("Applying object lock configuration to long term storage S3 bucket with default retention period of %d years\n", defaultObjectLockRetentionYears) @@ -160,13 +178,17 @@ func createLTSBucket(ctx context.Context, clt BootstrapS3Client, bucketName stri // policy is created that cleans up transient storage: // * Query results expire after 1 day // * DeleteMarkers, NonCurrentVersions and IncompleteMultipartUploads are also removed -func createTransientBucket(ctx context.Context, clt BootstrapS3Client, bucketName string, region string) error { +func createTransientBucket(ctx context.Context, clt BootstrapS3Client, bucketName string, region string, ownershipTags []s3types.Tag) error { fmt.Printf("Creating transient storage S3 bucket %s\n", bucketName) - err := createBucket(ctx, clt, bucketName, region, false) - if err != nil && !trace.IsAlreadyExists(err) { + err := ensureBucket(ctx, clt, bucketName, region, false) + if err != nil { return trace.Wrap(err, "creating transient S3 bucket") } + if err := applyBucketProperties(ctx, clt, bucketName, ownershipTags); err != nil { + return trace.Wrap(err, "applying S3 bucket properties") + } + fmt.Println("Applying bucket lifecycle configuration to transient storage S3 bucket") _, err = clt.PutBucketLifecycleConfiguration(ctx, &s3.PutBucketLifecycleConfigurationInput{ Bucket: &bucketName, @@ -202,7 +224,7 @@ func createTransientBucket(ctx context.Context, clt BootstrapS3Client, bucketNam return trace.Wrap(awsutil.ConvertS3Error(err), "setting lifecycle configuration on S3 bucket") } -func createBucket(ctx context.Context, clt BootstrapS3Client, bucketName string, region string, objectLock bool) error { +func ensureBucket(ctx context.Context, clt BootstrapS3Client, bucketName string, region string, objectLock bool) error { _, err := clt.CreateBucket(ctx, &s3.CreateBucketInput{ Bucket: &bucketName, CreateBucketConfiguration: awsutil.CreateBucketConfiguration(region), @@ -210,25 +232,48 @@ func createBucket(ctx context.Context, clt BootstrapS3Client, bucketName string, ACL: s3types.BucketCannedACLPrivate, ObjectOwnership: s3types.ObjectOwnershipBucketOwnerEnforced, }) - if err != nil { + convertedErr := awsutil.ConvertS3Error(err) + switch { + case convertedErr == nil: + return nil + case trace.IsAlreadyExists(convertedErr): + return nil + default: + return trace.Wrap(convertedErr) + } +} + +func applyBucketProperties(ctx context.Context, clt BootstrapS3Client, bucketName string, ownershipTags []s3types.Tag) error { + // s3:CreateBucket doesn't support tags, so the best we can do is + // to tag the bucket shortly after creating it + fmt.Printf("Adding tags to S3 bucket %s\n", bucketName) + if _, err := clt.PutBucketTagging(ctx, &s3.PutBucketTaggingInput{ + Bucket: &bucketName, + Tagging: &s3types.Tagging{TagSet: ownershipTags}, + }); err != nil { return trace.Wrap(awsutil.ConvertS3Error(err)) } - _, err = clt.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{ + fmt.Printf("Enabling Bucket versioning to S3 bucket %s\n", bucketName) + if _, err := clt.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{ Bucket: &bucketName, VersioningConfiguration: &s3types.VersioningConfiguration{ Status: s3types.BucketVersioningStatusEnabled, }, - }) - return trace.Wrap(awsutil.ConvertS3Error(err), "setting versioning configuration on S3 bucket") + }); err != nil { + return trace.Wrap(awsutil.ConvertS3Error(err), "setting versioning configuration on S3 bucket") + } + + return nil } // createAthenaWorkgroup creates an athena workgroup in which to run athena sql queries. -func createAthenaWorkgroup(ctx context.Context, clt BootstrapAthenaClient, workgroup string) error { +func createAthenaWorkgroup(ctx context.Context, clt BootstrapAthenaClient, workgroup string, ownershipTags []athenatypes.Tag) error { fmt.Printf("Creating Athena workgroup %s\n", workgroup) _, err := clt.CreateWorkGroup(ctx, &athena.CreateWorkGroupInput{ Name: &workgroup, Configuration: &athenatypes.WorkGroupConfiguration{}, + Tags: ownershipTags, }) if err != nil && !strings.Contains(err.Error(), "is already created") { return trace.Wrap(err, "creating Athena workgroup") @@ -243,13 +288,14 @@ func createAthenaWorkgroup(ctx context.Context, clt BootstrapAthenaClient, workg // * CreateDatabase // * CreateTable // * UpdateTable -func createGlueInfra(ctx context.Context, clt BootstrapGlueClient, table, database, eventBucket string) error { +func createGlueInfra(ctx context.Context, clt BootstrapGlueClient, region, table, database, eventBucket string, ownershipTags map[string]string) error { fmt.Printf("Creating Glue database %s\n", database) _, err := clt.CreateDatabase(ctx, &glue.CreateDatabaseInput{ DatabaseInput: &gluetypes.DatabaseInput{ Name: &database, Description: aws.String(glueDatabaseDescription), }, + Tags: ownershipTags, }) if err != nil { var aee *gluetypes.AlreadyExistsException diff --git a/lib/integrations/externalauditstorage/bootstrap_test.go b/lib/integrations/externalauditstorage/bootstrap_test.go index acdb694ca97..3d3993394d9 100644 --- a/lib/integrations/externalauditstorage/bootstrap_test.go +++ b/lib/integrations/externalauditstorage/bootstrap_test.go @@ -20,6 +20,7 @@ package externalauditstorage_test import ( "context" + "maps" "net/url" "testing" @@ -138,11 +139,13 @@ func TestBootstrapInfra(t *testing.T) { athenaClt := &mockBootstrapAthenaClient{} glueClt := &mockBootstrapGlueClient{} err := externalauditstorage.BootstrapInfra(testCtx, externalauditstorage.BootstrapInfraParams{ - Athena: athenaClt, - Glue: glueClt, - S3: s3Clt, - Spec: tc.spec, - Region: tc.region, + Athena: athenaClt, + Glue: glueClt, + S3: s3Clt, + Spec: tc.spec, + Region: tc.region, + ClusterName: "my-cluster", + IntegrationName: "my-integration", }) if tc.errWanted != "" { require.ErrorContainsf(t, err, tc.errWanted, "the error returned did not contain: %s", tc.errWanted) @@ -175,12 +178,45 @@ func TestBootstrapInfra(t *testing.T) { // Re-run bootstrap assert.NoError(t, externalauditstorage.BootstrapInfra(testCtx, externalauditstorage.BootstrapInfraParams{ - Athena: athenaClt, - Glue: glueClt, - S3: s3Clt, - Spec: tc.spec, - Region: tc.region, + Athena: athenaClt, + Glue: glueClt, + S3: s3Clt, + Spec: tc.spec, + Region: tc.region, + ClusterName: "my-cluster", + IntegrationName: "my-integration", })) + + // Enrure ownership tags were set on all resources. + // S3 Buckets + for bucketName, bucket := range s3Clt.buckets { + require.ElementsMatch(t, + []s3types.Tag{ + {Key: aws.String("teleport.dev/cluster"), Value: aws.String("my-cluster")}, + {Key: aws.String("teleport.dev/origin"), Value: aws.String("integration_awsoidc")}, + {Key: aws.String("teleport.dev/integration"), Value: aws.String("my-integration")}, + }, + bucket.tags, + "tags in bucket=%s do not match the ownership tags", bucketName) + } + // Athena Workgroup + require.ElementsMatch(t, + []athenatypes.Tag{ + {Key: aws.String("teleport.dev/cluster"), Value: aws.String("my-cluster")}, + {Key: aws.String("teleport.dev/origin"), Value: aws.String("integration_awsoidc")}, + {Key: aws.String("teleport.dev/integration"), Value: aws.String("my-integration")}, + }, + athenaClt.tags, + ) + // Glue Database + require.Equal(t, + map[string]string{ + "teleport.dev/cluster": "my-cluster", + "teleport.dev/origin": "integration_awsoidc", + "teleport.dev/integration": "my-integration", + }, + glueClt.databaseTags, + ) }) } } @@ -191,15 +227,18 @@ type mockBootstrapS3Client struct { type bucket struct { locationConstraint s3types.BucketLocationConstraint + tags []s3types.Tag } type mockBootstrapAthenaClient struct { workgroup string + tags []athenatypes.Tag } type mockBootstrapGlueClient struct { - table string - database string + table string + database string + databaseTags map[string]string } func (c *mockBootstrapS3Client) CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) { @@ -219,6 +258,19 @@ func (c *mockBootstrapS3Client) CreateBucket(ctx context.Context, params *s3.Cre return &s3.CreateBucketOutput{}, nil } +func (c *mockBootstrapS3Client) PutBucketTagging(ctx context.Context, params *s3.PutBucketTaggingInput, optFns ...func(*s3.Options)) (*s3.PutBucketTaggingOutput, error) { + bucket, ok := c.buckets[*params.Bucket] + if !ok { + // bucket doesn't exist return no such bucket error + return nil, &s3types.NoSuchBucket{Message: aws.String("The bucket doesn't exist")} + } + + bucket.tags = params.Tagging.TagSet + c.buckets[*params.Bucket] = bucket + + return &s3.PutBucketTaggingOutput{}, nil +} + func (c *mockBootstrapS3Client) PutObjectLockConfiguration(ctx context.Context, params *s3.PutObjectLockConfigurationInput, optFns ...func(*s3.Options)) (*s3.PutObjectLockConfigurationOutput, error) { if _, ok := c.buckets[*params.Bucket]; !ok { // bucket doesn't exist return no such bucket error @@ -251,6 +303,7 @@ func (c *mockBootstrapAthenaClient) CreateWorkGroup(ctx context.Context, params } c.workgroup = *params.Name + c.tags = params.Tags return &athena.CreateWorkGroupOutput{}, nil } @@ -280,6 +333,7 @@ func (c *mockBootstrapGlueClient) CreateDatabase(ctx context.Context, params *gl } c.database = *params.DatabaseInput.Name + c.databaseTags = maps.Clone(params.Tags) return &glue.CreateDatabaseOutput{}, nil } diff --git a/lib/integrations/externalauditstorage/easconfig/externalauditstroageconfig.go b/lib/integrations/externalauditstorage/easconfig/externalauditstroageconfig.go index b5f161ca79e..aa5113acfec 100644 --- a/lib/integrations/externalauditstorage/easconfig/externalauditstroageconfig.go +++ b/lib/integrations/externalauditstorage/easconfig/externalauditstroageconfig.go @@ -25,6 +25,12 @@ type ExternalAuditStorageConfiguration struct { Bootstrap bool // Region is the AWS Region used. Region string + // ClusterName is the Teleport cluster name. + // Used for resource tagging. + ClusterName string + // IntegrationName is the Teleport AWS OIDC Integration name. + // Used for resource tagging. + IntegrationName string // Role is the AWS IAM Role associated with the OIDC integration. Role string // Policy is the name to use for the IAM policy. diff --git a/tool/teleport/common/integration_configure.go b/tool/teleport/common/integration_configure.go index 5b96eb20d9e..27d2134e0bc 100644 --- a/tool/teleport/common/integration_configure.go +++ b/tool/teleport/common/integration_configure.go @@ -191,7 +191,9 @@ func onIntegrationConfExternalAuditCmd(ctx context.Context, params easconfig.Ext GlueDatabase: params.GlueDatabase, GlueTable: params.GlueTable, }, - Region: params.Region, + Region: params.Region, + ClusterName: params.ClusterName, + IntegrationName: params.IntegrationName, }) if err != nil { return trace.Wrap(err) diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index 0bec31292ff..f5ba91e7b5e 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -518,6 +518,8 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con integrationConfExternalAuditCmd := integrationConfigureCmd.Command("externalauditstorage", "Bootstraps required infrastructure and adds required IAM permissions for External Audit Storage logs.") integrationConfExternalAuditCmd.Flag("bootstrap", "Bootstrap required infrastructure.").Default("false").BoolVar(&ccf.IntegrationConfExternalAuditStorageArguments.Bootstrap) integrationConfExternalAuditCmd.Flag("aws-region", "AWS region.").Required().StringVar(&ccf.IntegrationConfExternalAuditStorageArguments.Region) + integrationConfExternalAuditCmd.Flag("cluster-name", "Teleport Cluster name.").Required().StringVar(&ccf.IntegrationConfExternalAuditStorageArguments.ClusterName) + integrationConfExternalAuditCmd.Flag("integration", "AWS OIDC Integration name.").Required().StringVar(&ccf.IntegrationConfExternalAuditStorageArguments.IntegrationName) integrationConfExternalAuditCmd.Flag("role", "The IAM Role used by the AWS OIDC Integration.").Required().StringVar(&ccf.IntegrationConfExternalAuditStorageArguments.Role) integrationConfExternalAuditCmd.Flag("policy", "The name for the Policy to attach to the IAM role.").Required().StringVar(&ccf.IntegrationConfExternalAuditStorageArguments.Policy) integrationConfExternalAuditCmd.Flag("session-recordings", "The S3 URI where session recordings are stored.").Required().StringVar(&ccf.IntegrationConfExternalAuditStorageArguments.SessionRecordingsURI)