diff --git a/.changelog/39060.txt b/.changelog/39060.txt new file mode 100644 index 00000000000..a7536c43843 --- /dev/null +++ b/.changelog/39060.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +data-source/aws_workspaces_directory: Add `saml_properties` attribute +``` + +```release-note:enhancement +resource/aws_workspaces_directory: Add `saml_properties` configuration block +``` \ No newline at end of file diff --git a/internal/service/workspaces/README.md b/internal/service/workspaces/README.md index e406bd060c6..5a7b435082b 100644 --- a/internal/service/workspaces/README.md +++ b/internal/service/workspaces/README.md @@ -2,6 +2,21 @@ This area is primarily for AWS provider contributors and maintainers. For information on _using_ Terraform and the AWS provider, see the links below. +Acceptance tests for the following resource types are bundled into the `TestAccWorkSpaces_serial` test: + +* `aws_workspaces_directory` +* `aws_workspaces_ip_group` +* `aws_workspaces_workspace` + +Acceptance tests for the following data sources are bundled into the `TestAccWorkSpacesDataSource_serial` test: + +* `aws_workspaces_bundle` +* `aws_workspaces_directory` +* `aws_workspaces_image` +* `aws_workspaces_workspace` + +To invoke specific tests in a bundle, use the subtest specification syntax (`/` or `//`). + ## Handy Links * [Find out about contributing](https://hashicorp.github.io/terraform-provider-aws/#contribute) to the AWS provider! diff --git a/internal/service/workspaces/directory.go b/internal/service/workspaces/directory.go index f76aee5b6ab..588ae5cdb0e 100644 --- a/internal/service/workspaces/directory.go +++ b/internal/service/workspaces/directory.go @@ -81,6 +81,31 @@ func resourceDirectory() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "saml_properties": { + Type: schema.TypeList, + Computed: true, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "relay_state_parameter_name": { + Type: schema.TypeString, + Optional: true, + Default: "RelayState", + }, + names.AttrStatus: { + Type: schema.TypeString, + Optional: true, + Default: types.SamlStatusEnumDisabled, + ValidateDiagFunc: enum.Validate[types.SamlStatusEnum](), + }, + "user_access_url": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, "self_service_permissions": { Type: schema.TypeList, Computed: true, @@ -253,6 +278,19 @@ func resourceDirectoryCreate(ctx context.Context, d *schema.ResourceData, meta i return sdkdiag.AppendErrorf(diags, "waiting for WorkSpaces Directory (%s) create: %s", d.Id(), err) } + if v, ok := d.GetOk("saml_properties"); ok { + input := &workspaces.ModifySamlPropertiesInput{ + ResourceId: aws.String(d.Id()), + SamlProperties: expandSAMLProperties(v.([]interface{})), + } + + _, err := conn.ModifySamlProperties(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "setting WorkSpaces Directory (%s) SAML properties: %s", d.Id(), err) + } + } + if v, ok := d.GetOk("self_service_permissions"); ok { input := &workspaces.ModifySelfservicePermissionsInput{ ResourceId: aws.String(d.Id()), @@ -335,6 +373,9 @@ func resourceDirectoryRead(ctx context.Context, d *schema.ResourceData, meta int if err := d.Set("self_service_permissions", flattenSelfservicePermissions(directory.SelfservicePermissions)); err != nil { return sdkdiag.AppendErrorf(diags, "setting self_service_permissions: %s", err) } + if err := d.Set("saml_properties", flattenSAMLProperties(directory.SamlProperties)); err != nil { + return sdkdiag.AppendErrorf(diags, "setting saml_properties: %s", err) + } d.Set(names.AttrSubnetIDs, directory.SubnetIds) if err := d.Set("workspace_access_properties", flattenWorkspaceAccessProperties(directory.WorkspaceAccessProperties)); err != nil { return sdkdiag.AppendErrorf(diags, "setting workspace_access_properties: %s", err) @@ -351,6 +392,31 @@ func resourceDirectoryUpdate(ctx context.Context, d *schema.ResourceData, meta i var diags diag.Diagnostics conn := meta.(*conns.AWSClient).WorkSpacesClient(ctx) + if d.HasChange("saml_properties") { + tfListSAMLProperties := d.Get("saml_properties").([]interface{}) + tfMap := tfListSAMLProperties[0].(map[string]interface{}) + + var dels []types.DeletableSamlProperty + if tfMap["relay_state_parameter_name"].(string) == "" { + dels = append(dels, types.DeletableSamlPropertySamlPropertiesRelayStateParameterName) + } + if tfMap["user_access_url"].(string) == "" { + dels = append(dels, types.DeletableSamlPropertySamlPropertiesUserAccessUrl) + } + + input := &workspaces.ModifySamlPropertiesInput{ + PropertiesToDelete: dels, + ResourceId: aws.String(d.Id()), + SamlProperties: expandSAMLProperties(tfListSAMLProperties), + } + + _, err := conn.ModifySamlProperties(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "updating WorkSpaces Directory (%s) SAML properties: %s", d.Id(), err) + } + } + if d.HasChange("self_service_permissions") { input := &workspaces.ModifySelfservicePermissionsInput{ ResourceId: aws.String(d.Id()), @@ -614,6 +680,29 @@ func expandWorkspaceAccessProperties(tfList []interface{}) *types.WorkspaceAcces return apiObject } +func expandSAMLProperties(tfList []interface{}) *types.SamlProperties { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap := tfList[0].(map[string]interface{}) + apiObject := &types.SamlProperties{} + + if tfMap["relay_state_parameter_name"].(string) != "" { + apiObject.RelayStateParameterName = aws.String(tfMap["relay_state_parameter_name"].(string)) + } + + if tfMap[names.AttrStatus].(string) != "" { + apiObject.Status = types.SamlStatusEnum(tfMap[names.AttrStatus].(string)) + } + + if tfMap["user_access_url"].(string) != "" { + apiObject.UserAccessUrl = aws.String(tfMap["user_access_url"].(string)) + } + + return apiObject +} + func expandSelfservicePermissions(tfList []interface{}) *types.SelfservicePermissions { if len(tfList) == 0 || tfList[0] == nil { return nil @@ -697,6 +786,20 @@ func flattenWorkspaceAccessProperties(apiObject *types.WorkspaceAccessProperties } } +func flattenSAMLProperties(apiObject *types.SamlProperties) []interface{} { + if apiObject == nil { + return []interface{}{} + } + + return []interface{}{ + map[string]interface{}{ + "relay_state_parameter_name": aws.ToString(apiObject.RelayStateParameterName), + names.AttrStatus: apiObject.Status, + "user_access_url": aws.ToString(apiObject.UserAccessUrl), + }, + } +} + func flattenSelfservicePermissions(apiObject *types.SelfservicePermissions) []interface{} { if apiObject == nil { return []interface{}{} diff --git a/internal/service/workspaces/directory_data_source.go b/internal/service/workspaces/directory_data_source.go index cf30c95fc16..b093da1e50a 100644 --- a/internal/service/workspaces/directory_data_source.go +++ b/internal/service/workspaces/directory_data_source.go @@ -59,6 +59,26 @@ func dataSourceDirectory() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "saml_properties": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "relay_state_parameter_name": { + Type: schema.TypeString, + Computed: true, + }, + names.AttrStatus: { + Type: schema.TypeString, + Computed: true, + }, + "user_access_url": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, "self_service_permissions": { Type: schema.TypeList, Computed: true, @@ -192,6 +212,9 @@ func dataSourceDirectoryRead(ctx context.Context, d *schema.ResourceData, meta i if err := d.Set("self_service_permissions", flattenSelfservicePermissions(directory.SelfservicePermissions)); err != nil { return sdkdiag.AppendErrorf(diags, "setting self_service_permissions: %s", err) } + if err := d.Set("saml_properties", flattenSAMLProperties(directory.SamlProperties)); err != nil { + return sdkdiag.AppendErrorf(diags, "setting saml_properties: %s", err) + } d.Set(names.AttrSubnetIDs, directory.SubnetIds) if err := d.Set("workspace_access_properties", flattenWorkspaceAccessProperties(directory.WorkspaceAccessProperties)); err != nil { return sdkdiag.AppendErrorf(diags, "setting workspace_access_properties: %s", err) diff --git a/internal/service/workspaces/directory_data_source_test.go b/internal/service/workspaces/directory_data_source_test.go index 062729fc636..a472e0b4195 100644 --- a/internal/service/workspaces/directory_data_source_test.go +++ b/internal/service/workspaces/directory_data_source_test.go @@ -44,6 +44,10 @@ func testAccDirectoryDataSource_basic(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceName, "iam_role_id", resourceName, "iam_role_id"), resource.TestCheckResourceAttrPair(dataSourceName, "ip_group_ids", resourceName, "ip_group_ids"), resource.TestCheckResourceAttrPair(dataSourceName, "registration_code", resourceName, "registration_code"), + resource.TestCheckResourceAttrPair(dataSourceName, "saml_properties.#", resourceName, "saml_properties.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "saml_properties.0.relay_state_parameter_name", resourceName, "saml_properties.0.relay_state_parameter_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "saml_properties.0.status", resourceName, "saml_properties.0.status"), + resource.TestCheckResourceAttrPair(dataSourceName, "saml_properties.0.user_access_url", resourceName, "saml_properties.0.user_access_url"), resource.TestCheckResourceAttrPair(dataSourceName, "self_service_permissions.#", resourceName, "self_service_permissions.#"), resource.TestCheckResourceAttrPair(dataSourceName, "self_service_permissions.0.change_compute_type", resourceName, "self_service_permissions.0.change_compute_type"), resource.TestCheckResourceAttrPair(dataSourceName, "self_service_permissions.0.increase_volume_size", resourceName, "self_service_permissions.0.increase_volume_size"), @@ -90,6 +94,12 @@ resource "aws_security_group" "test" { resource "aws_workspaces_directory" "test" { directory_id = aws_directory_service_directory.main.id + saml_properties { + relay_state_parameter_name = "LinkMode" + status = "ENABLED" + user_access_url = "https://sso.%[2]s/" + } + self_service_permissions { change_compute_type = false increase_volume_size = true @@ -129,5 +139,5 @@ data "aws_workspaces_directory" "test" { data "aws_iam_role" "workspaces-default" { name = "workspaces_DefaultRole" } -`, rName)) +`, rName, domain)) } diff --git a/internal/service/workspaces/directory_test.go b/internal/service/workspaces/directory_test.go index 1721f9f174b..0565106384d 100644 --- a/internal/service/workspaces/directory_test.go +++ b/internal/service/workspaces/directory_test.go @@ -55,6 +55,9 @@ func testAccDirectory_basic(t *testing.T) { resource.TestCheckResourceAttrPair(resourceName, "iam_role_id", iamRoleDataSourceName, names.AttrARN), resource.TestCheckResourceAttr(resourceName, "ip_group_ids.#", acctest.Ct0), resource.TestCheckResourceAttrSet(resourceName, "registration_code"), + resource.TestCheckResourceAttr(resourceName, "saml_properties.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.relay_state_parameter_name", "RelayState"), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.status", "DISABLED"), resource.TestCheckResourceAttr(resourceName, "self_service_permissions.#", acctest.Ct1), resource.TestCheckResourceAttr(resourceName, "self_service_permissions.0.change_compute_type", acctest.CtFalse), resource.TestCheckResourceAttr(resourceName, "self_service_permissions.0.increase_volume_size", acctest.CtFalse), @@ -213,6 +216,84 @@ func testAccDirectory_tags(t *testing.T) { }) } +func testAccDirectory_SamlProperties(t *testing.T) { + ctx := acctest.Context(t) + var v types.WorkspaceDirectory + rName := sdkacctest.RandString(8) + + resourceName := "aws_workspaces_directory.main" + + domain := acctest.RandomDomainName() + rspn := sdkacctest.RandString(8) + arspn := sdkacctest.RandString(8) + uau := fmt.Sprintf("https://%s/", acctest.RandomDomainName()) + auau := fmt.Sprintf("https://%s/", acctest.RandomDomainName()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckDirectory(ctx, t) + acctest.PreCheckDirectoryServiceSimpleDirectory(ctx, t) + acctest.PreCheckHasIAMRole(ctx, t, "workspaces_DefaultRole") + }, + ErrorCheck: acctest.ErrorCheck(t, strings.ToLower(workspaces.ServiceID)), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDirectoryDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDirectoryConfig_samlPropertiesFull(rName, domain, rspn, uau), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDirectoryExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "saml_properties.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.relay_state_parameter_name", rspn), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.user_access_url", uau), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.status", "ENABLED"), + ), + }, + { + Config: testAccDirectoryConfig_samlPropertiesRSPN(rName, domain, arspn), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDirectoryExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "saml_properties.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.relay_state_parameter_name", arspn), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.user_access_url", ""), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.status", "DISABLED"), + ), + }, + { + Config: testAccDirectoryConfig_samlPropertiesUAU(rName, domain, auau), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDirectoryExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "saml_properties.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.relay_state_parameter_name", "RelayState"), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.user_access_url", auau), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.status", "ENABLED_WITH_DIRECTORY_LOGIN_FALLBACK"), + ), + }, + { + Config: testAccDirectoryConfig_samlPropertiesFull(rName, domain, rspn, uau), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDirectoryExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "saml_properties.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.relay_state_parameter_name", rspn), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.user_access_url", uau), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.status", "ENABLED"), + ), + }, + { + Config: testAccDirectoryConfig_samlPropertiesEmpty(rName, domain), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDirectoryExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "saml_properties.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.relay_state_parameter_name", "RelayState"), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.user_access_url", ""), + resource.TestCheckResourceAttr(resourceName, "saml_properties.0.status", "DISABLED"), + ), + }, + }, + }) +} + func testAccDirectory_selfServicePermissions(t *testing.T) { ctx := acctest.Context(t) var v types.WorkspaceDirectory @@ -565,6 +646,80 @@ data "aws_iam_role" "workspaces-default" { `, rName)) } +func testAccDirectoryConfig_samlPropertiesFull(rName, domain, rspn, uau string) string { + return acctest.ConfigCompose( + testAccDirectoryConfig_Prerequisites(rName, domain), + fmt.Sprintf(` +resource "aws_workspaces_directory" "main" { + directory_id = aws_directory_service_directory.main.id + + saml_properties { + relay_state_parameter_name = %[2]q + user_access_url = %[3]q + status = "ENABLED" + } + + tags = { + Name = "tf-testacc-workspaces-directory-%[1]s" + } +} +`, rName, rspn, uau)) +} + +func testAccDirectoryConfig_samlPropertiesRSPN(rName, domain, rspn string) string { + return acctest.ConfigCompose( + testAccDirectoryConfig_Prerequisites(rName, domain), + fmt.Sprintf(` +resource "aws_workspaces_directory" "main" { + directory_id = aws_directory_service_directory.main.id + + saml_properties { + relay_state_parameter_name = %[2]q + status = "DISABLED" + } + + tags = { + Name = "tf-testacc-workspaces-directory-%[1]s" + } +} +`, rName, rspn)) +} + +func testAccDirectoryConfig_samlPropertiesUAU(rName, domain, uau string) string { + return acctest.ConfigCompose( + testAccDirectoryConfig_Prerequisites(rName, domain), + fmt.Sprintf(` +resource "aws_workspaces_directory" "main" { + directory_id = aws_directory_service_directory.main.id + + saml_properties { + user_access_url = %[2]q + status = "ENABLED_WITH_DIRECTORY_LOGIN_FALLBACK" + } + + tags = { + Name = "tf-testacc-workspaces-directory-%[1]s" + } +} +`, rName, uau)) +} + +func testAccDirectoryConfig_samlPropertiesEmpty(rName, domain string) string { + return acctest.ConfigCompose( + testAccDirectoryConfig_Prerequisites(rName, domain), + fmt.Sprintf(` +resource "aws_workspaces_directory" "main" { + directory_id = aws_directory_service_directory.main.id + + saml_properties {} + + tags = { + Name = "tf-testacc-workspaces-directory-%[1]s" + } +} +`, rName)) +} + func testAccDirectoryConfig_selfServicePermissions(rName, domain string) string { return acctest.ConfigCompose( testAccDirectoryConfig_Prerequisites(rName, domain), diff --git a/internal/service/workspaces/workspaces_test.go b/internal/service/workspaces/workspaces_test.go index 3a342d84073..6946d304304 100644 --- a/internal/service/workspaces/workspaces_test.go +++ b/internal/service/workspaces/workspaces_test.go @@ -23,6 +23,7 @@ func TestAccWorkSpaces_serial(t *testing.T) { "workspaceAccessProperties": testAccDirectory_workspaceAccessProperties, "workspaceCreationProperties": testAccDirectory_workspaceCreationProperties, "workspaceCreationProperties_customSecurityGroupId_defaultOu": testAccDirectory_workspaceCreationProperties_customSecurityGroupId_defaultOu, + "workspaceSamlProperties": testAccDirectory_SamlProperties, }, "IpGroup": { acctest.CtBasic: testAccIPGroup_basic, diff --git a/website/docs/r/workspaces_directory.html.markdown b/website/docs/r/workspaces_directory.html.markdown index 51cda8313e3..b29ffff28f4 100644 --- a/website/docs/r/workspaces_directory.html.markdown +++ b/website/docs/r/workspaces_directory.html.markdown @@ -26,6 +26,11 @@ resource "aws_workspaces_directory" "example" { Example = true } + saml_properties { + user_access_url = "https://sso.example.com/" + status = "ENABLED" + } + self_service_permissions { change_compute_type = true increase_volume_size = true @@ -149,12 +154,19 @@ This resource supports the following arguments: * `directory_id` - (Required) The directory identifier for registration in WorkSpaces service. * `subnet_ids` - (Optional) The identifiers of the subnets where the directory resides. -* `ip_group_ids` - The identifiers of the IP access control groups associated with the directory. +* `ip_group_ids` – (Optional) The identifiers of the IP access control groups associated with the directory. * `tags` – (Optional) A map of tags assigned to the WorkSpaces directory. 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. +* `saml_properties` – (Optional) Configuration of SAML authentication integration. Defined below. * `self_service_permissions` – (Optional) Permissions to enable or disable self-service capabilities. Defined below. * `workspace_access_properties` – (Optional) Specifies which devices and operating systems users can use to access their WorkSpaces. Defined below. * `workspace_creation_properties` – (Optional) Default properties that are used for creating WorkSpaces. Defined below. +### saml_properties + +* `relay_state_parameter_name` - (Optional) The relay state parameter name supported by the SAML 2.0 identity provider (IdP). Default `RelayState`. +* `status` - (Optional) Status of SAML 2.0 authentication. Default `DISABLED`. +* `user_access_url` - (Optional) The SAML 2.0 identity provider (IdP) user access URL. + ### self_service_permissions * `change_compute_type` – (Optional) Whether WorkSpaces directory users can change the compute type (bundle) for their workspace. Default `false`.