Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

F aws transfer server structured logging destinations #32654

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/32654.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/aws_transfer_server: Add `structured_log_destinations` argument
```

```release-note:enhancement
data-source/aws_transfer_server: Add `structured_log_destinations` attribute
```
19 changes: 19 additions & 0 deletions internal/service/transfer/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,15 @@ func ResourceServer() *schema.Resource {
Default: SecurityPolicyName2018_11,
ValidateFunc: validation.StringInSlice(SecurityPolicyName_Values(), false),
},
"structured_log_destinations": {
Type: schema.TypeSet,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: verify.ValidARN,
},
Description: "This is a set of arns of destinations that will receive structured logs from the transfer server",
Optional: true,
},
names.AttrTags: tftags.TagsSchema(),
names.AttrTagsAll: tftags.TagsSchemaComputed(),
"url": {
Expand Down Expand Up @@ -371,6 +380,10 @@ func resourceServerCreate(ctx context.Context, d *schema.ResourceData, meta inte
input.SecurityPolicyName = aws.String(v.(string))
}

if v, ok := d.GetOk("structured_log_destinations"); ok && v.(*schema.Set).Len() > 0 {
input.StructuredLogDestinations = flex.ExpandStringSet(v.(*schema.Set))
}

if v, ok := d.GetOk("url"); ok {
if input.IdentityProviderDetails == nil {
input.IdentityProviderDetails = &transfer.IdentityProviderDetails{}
Expand Down Expand Up @@ -489,6 +502,7 @@ func resourceServerRead(ctx context.Context, d *schema.ResourceData, meta interf
}
d.Set("protocols", aws.StringValueSlice(output.Protocols))
d.Set("security_policy_name", output.SecurityPolicyName)
d.Set("structured_log_destinations", aws.StringValueSlice(output.StructuredLogDestinations))
if output.IdentityProviderDetails != nil {
d.Set("url", output.IdentityProviderDetails.Url)
} else {
Expand Down Expand Up @@ -666,6 +680,11 @@ func resourceServerUpdate(ctx context.Context, d *schema.ResourceData, meta inte
input.SecurityPolicyName = aws.String(d.Get("security_policy_name").(string))
}

// Per the docs it does not matter if this field has changed,
// if the update passes this as empty the structured logging will be turned off,
// so we need to always pass the new.
input.StructuredLogDestinations = flex.ExpandStringSet(d.Get("structured_log_destinations").(*schema.Set))

if d.HasChange("workflow_details") {
input.WorkflowDetails = expandWorkflowDetails(d.Get("workflow_details").([]interface{}))
}
Expand Down
18 changes: 8 additions & 10 deletions internal/service/transfer/server_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ func DataSourceServer() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},

"certificate": {
Type: schema.TypeString,
Computed: true,
Expand All @@ -31,50 +30,48 @@ func DataSourceServer() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},

"endpoint": {
Type: schema.TypeString,
Computed: true,
},

"endpoint_type": {
Type: schema.TypeString,
Computed: true,
},

"identity_provider_type": {
Type: schema.TypeString,
Computed: true,
},

"invocation_role": {
Type: schema.TypeString,
Computed: true,
},

"logging_role": {
Type: schema.TypeString,
Computed: true,
},

"protocols": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},

"security_policy_name": {
Type: schema.TypeString,
Computed: true,
},

"server_id": {
Type: schema.TypeString,
Required: true,
},

"structured_log_destinations": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"url": {
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -112,6 +109,7 @@ func dataSourceServerRead(ctx context.Context, d *schema.ResourceData, meta inte
d.Set("logging_role", output.LoggingRole)
d.Set("protocols", aws.StringValueSlice(output.Protocols))
d.Set("security_policy_name", output.SecurityPolicyName)
d.Set("structured_log_destinations", aws.StringValueSlice(output.StructuredLogDestinations))
if output.IdentityProviderDetails != nil {
d.Set("url", output.IdentityProviderDetails.Url)
} else {
Expand Down
1 change: 1 addition & 0 deletions internal/service/transfer/server_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func testAccServerDataSource_basic(t *testing.T) {
resource.TestCheckResourceAttrPair(datasourceName, "endpoint", resourceName, "endpoint"),
resource.TestCheckResourceAttrPair(datasourceName, "identity_provider_type", resourceName, "identity_provider_type"),
resource.TestCheckResourceAttrPair(datasourceName, "logging_role", resourceName, "logging_role"),
resource.TestCheckResourceAttrPair(datasourceName, "structured_log_destinations.#", resourceName, "structured_log_destinations.#"),
),
},
},
Expand Down
154 changes: 154 additions & 0 deletions internal/service/transfer/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package transfer_test

import (
"context"
"errors"
"fmt"
"regexp"
"testing"
Expand Down Expand Up @@ -71,6 +72,7 @@ func testAccServer_basic(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "protocols.#", "1"),
resource.TestCheckTypeSetElemAttr(resourceName, "protocols.*", "SFTP"),
resource.TestCheckResourceAttr(resourceName, "security_policy_name", "TransferSecurityPolicy-2018-11"),
resource.TestCheckResourceAttr(resourceName, "structured_log_destinations.#", "0"),
resource.TestCheckResourceAttr(resourceName, "tags.%", "0"),
resource.TestCheckResourceAttr(resourceName, "url", ""),
resource.TestCheckResourceAttr(resourceName, "workflow_details.#", "0"),
Expand Down Expand Up @@ -740,6 +742,46 @@ func testAccServer_updateEndpointType_vpcToPublic(t *testing.T) {
})
}

func testAccServer_structuredLogDestinations(t *testing.T) {
ctx := acctest.Context(t)
var s transfer.DescribedServer
resourceName := "aws_transfer_server.test"
cloudwatchLogGroupName := "aws_cloudwatch_log_group.test"
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, transfer.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckServerDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccServerConfig_structuredLogDestinations(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckServerExists(ctx, resourceName, &s),
// resource.TestCheckTypeSetElemAttr(resourceName, "structured_logging_destinations.*", *s.StructuredLogDestinations[0]),
resource.ComposeTestCheckFunc(testAccServerCheck_structuredLogDestinations(resourceName, cloudwatchLogGroupName)),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"force_destroy"},
},
{
Config: testAccServerConfig_structuredLogDestinationsUpdate(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckServerExists(ctx, resourceName, &s),
// resource.TestCheckTypeSetElemAttr(resourceName, "structured_logging_destinations.*", *s.StructuredLogDestinations[0]),
// resource.TestCheckTypeSetElemAttr(resourceName, "structured_logging_destinations.*", fmt.Sprintf("\"${%s.arn}:*\"", cloudwatchLogGroupName)),
resource.ComposeTestCheckFunc(testAccServerCheck_structuredLogDestinations(resourceName, cloudwatchLogGroupName)),
),
},
},
})
}

func testAccServer_protocols(t *testing.T) {
ctx := acctest.Context(t)
var s transfer.DescribedServer
Expand Down Expand Up @@ -1219,6 +1261,32 @@ func testAccCheckServerDestroy(ctx context.Context) resource.TestCheckFunc {
}
}

func testAccServerCheck_structuredLogDestinations(resourceName, cloudwatchLogGroupName string) func(s *terraform.State) error {
return func(s *terraform.State) error {
cwResource, ok := s.RootModule().Resources[cloudwatchLogGroupName]
if !ok {
return fmt.Errorf("resource not found: %s", cloudwatchLogGroupName)
}
cwARN, ok := cwResource.Primary.Attributes["arn"]
if !ok {
return errors.New("cloudwatch group arn missing")
}
expectedSLD := fmt.Sprintf("%s:*", cwARN)
transferServerResource, ok := s.RootModule().Resources[resourceName]
if !ok {
return fmt.Errorf("resource not found: %s", resourceName)
}
slds, ok := transferServerResource.Primary.Attributes["structured_log_destinations.0"]
if !ok {
return errors.New("transfer server structured logging destinations missing")
}
if expectedSLD != slds {
return fmt.Errorf("'%s' != '%s'", expectedSLD, slds)
}
return nil
}
}

func testAccServerConfig_vpcBase(rName string) string {
return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(`
resource "aws_vpc" "test" {
Expand Down Expand Up @@ -1784,6 +1852,92 @@ resource "aws_transfer_server" "test" {
`, rName, hostKey)
}

func testAccServerConfig_structuredLogDestinationsBase(rName string) string {
return fmt.Sprintf(`
resource "aws_cloudwatch_log_group" "test" {
name = %[1]q
}

data "aws_iam_policy_document" "test" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["transfer.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}

resource "aws_iam_policy" "test" {
name = %[1]q

policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Action" : [
"logs:CreateLogStream",
"logs:DescribeLogStreams",
"logs:CreateLogGroup",
"logs:PutLogEvents"
],
"Resource" : "*"
}
]
})
}

resource "aws_iam_role" "test" {
name = %[1]q
assume_role_policy = data.aws_iam_policy_document.test.json
}

resource "aws_iam_role_policy_attachment" "test" {
role = aws_iam_role.test.name
policy_arn = aws_iam_policy.test.arn
}
`, rName)
}

func testAccServerConfig_structuredLogDestinations(rName string) string {
return acctest.ConfigCompose(testAccServerConfig_structuredLogDestinationsBase(rName), fmt.Sprintf(`
resource "aws_transfer_server" "test" {
endpoint_type = "PUBLIC"
logging_role = aws_iam_role.test.arn
protocols = ["SFTP"]
structured_log_destinations = [
"${aws_cloudwatch_log_group.test.arn}:*"
]

tags = {
Name = %[1]q
}
}
`, rName))
}

func testAccServerConfig_structuredLogDestinationsUpdate(rName string) string {
return acctest.ConfigCompose(testAccServerConfig_structuredLogDestinationsBase(rName), fmt.Sprintf(`
resource "aws_transfer_server" "test" {
endpoint_type = "PUBLIC"
logging_role = aws_iam_role.test.arn
protocols = ["SFTP"]
structured_log_destinations = [
"${aws_cloudwatch_log_group.test.arn}:*"
]

pre_authentication_login_banner = "This system is for the use of authorized users only - pre"
post_authentication_login_banner = "This system is for the use of authorized users only - post"

tags = {
Name = %[1]q
}
}
`, rName))
}

func testAccServerConfig_protocols(rName string) string {
return acctest.ConfigCompose(
testAccServerConfig_vpcBase(rName),
Expand Down
1 change: 1 addition & 0 deletions internal/service/transfer/transfer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func TestAccTransfer_serial(t *testing.T) {
"Protocols": testAccServer_protocols,
"ProtocolDetails": testAccServer_protocolDetails,
"SecurityPolicy": testAccServer_securityPolicy,
"StructuredLogDestinations": testAccServer_structuredLogDestinations,
"UpdateEndpointTypePublicToVPC": testAccServer_updateEndpointType_publicToVPC,
"UpdateEndpointTypePublicToVPCAddressAllocationIDs": testAccServer_updateEndpointType_publicToVPC_addressAllocationIDs,
"UpdateEndpointTypeVPCEndpointToVPC": testAccServer_updateEndpointType_vpcEndpointToVPC,
Expand Down
1 change: 1 addition & 0 deletions website/docs/d/transfer_server.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ This data source exports the following attributes in addition to the arguments a
* `logging_role` - ARN of an IAM role that allows the service to write your SFTP users’ activity to your Amazon CloudWatch logs for monitoring and auditing purposes.
* `protocols` - File transfer protocol or protocols over which your file transfer protocol client can connect to your server's endpoint.
* `security_policy_name` - The name of the security policy that is attached to the server.
* `structured_logging_destinations` - A set of ARNs of destinations that will receive structured logs from the transfer server such as CloudWatch Log Group ARNs.
* `url` - URL of the service endpoint used to authenticate users with an `identity_provider_type` of `API_GATEWAY`.
37 changes: 37 additions & 0 deletions website/docs/r/transfer_server.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,42 @@ resource "aws_transfer_server" "example" {
}
```

### Using Structured Logging Destinations

```terraform
resource "aws_cloudwatch_log_group" "transfer" {
name_prefix = "transfer_test_"
}

data "aws_iam_policy_document" "transfer_assume_role" {
statement {
effect = "Allow"

principals {
type = "Service"
identifiers = ["transfer.amazonaws.com"]
}

actions = ["sts:AssumeRole"]
}
}

resource "aws_iam_role" "iam_for_transfer" {
name_prefix = "iam_for_transfer_"
assume_role_policy = data.aws_iam_policy_document.transfer_assume_role.json
managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AWSTransferLoggingAccess"]
}

resource "aws_transfer_server" "transfer" {
endpoint_type = "PUBLIC"
logging_role = aws_iam_role.iam_for_transfer.arn
protocols = ["SFTP"]
structured_log_destinations = [
"${aws_cloudwatch_log_group.transfer.arn}:*"
]
}
```

## Argument Reference

The following arguments are supported:
Expand All @@ -110,6 +146,7 @@ The following arguments are supported:
* `pre_authentication_login_banner`- (Optional) Specify a string to display when users connect to a server. This string is displayed before the user authenticates.
* `protocol_details`- (Optional) The protocol settings that are configured for your server.
* `security_policy_name` - (Optional) Specifies the name of the security policy that is attached to the server. Possible values are `TransferSecurityPolicy-2018-11`, `TransferSecurityPolicy-2020-06`, `TransferSecurityPolicy-FIPS-2020-06`, `TransferSecurityPolicy-2022-03` and `TransferSecurityPolicy-2023-05`. Default value is: `TransferSecurityPolicy-2018-11`.
* `structured_logging_destinations` - (Optional) A set of ARNs of destinations that will receive structured logs from the transfer server such as CloudWatch Log Group ARNs. If provided this enables the transfer server to emit structured logs to the specified locations.
* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level.
* `workflow_details` - (Optional) Specifies the workflow details. See Workflow Details below.

Expand Down
Loading