From 5e7cac18b695eba27ee0473c982df9931c6b397e Mon Sep 17 00:00:00 2001 From: philof Date: Mon, 24 May 2021 11:22:27 -0400 Subject: [PATCH 1/6] new resource: aws_elasticsearch_domain_saml_options --- aws/elasticsearch_domain_structure.go | 94 ++++++ aws/provider.go | 1 + ...e_aws_elasticsearch_domain_saml_options.go | 218 ++++++++++++ ..._elasticsearch_domain_saml_options_test.go | 311 ++++++++++++++++++ aws/resource_aws_elasticsearch_domain_test.go | 14 +- 5 files changed, 637 insertions(+), 1 deletion(-) create mode 100644 aws/resource_aws_elasticsearch_domain_saml_options.go create mode 100644 aws/resource_aws_elasticsearch_domain_saml_options_test.go diff --git a/aws/elasticsearch_domain_structure.go b/aws/elasticsearch_domain_structure.go index 4762b465456..28b2c570c79 100644 --- a/aws/elasticsearch_domain_structure.go +++ b/aws/elasticsearch_domain_structure.go @@ -44,6 +44,57 @@ func expandAdvancedSecurityOptions(m []interface{}) *elasticsearch.AdvancedSecur return &config } +func expandESSAMLOptions(data []interface{}) *elasticsearch.SAMLOptionsInput { + if len(data) == 0 || data[0] == nil { + return nil + } + + options := elasticsearch.SAMLOptionsInput{} + group := data[0].(map[string]interface{}) + + if SAMLEnabled, ok := group["enabled"]; ok { + options.Enabled = aws.Bool(SAMLEnabled.(bool)) + + if SAMLEnabled.(bool) { + options.Idp = expandSAMLOptionsIdp(group["idp"].([]interface{})) + if v, ok := group["master_backend_role"].(string); ok && v != "" { + options.MasterBackendRole = aws.String(v) + } + if v, ok := group["master_user_name"].(string); ok && v != "" { + options.MasterUserName = aws.String(v) + } + if v, ok := group["roles_key"].(string); ok { + options.RolesKey = aws.String(v) + } + if v, ok := group["session_timeout_minutes"].(int); ok { + options.SessionTimeoutMinutes = aws.Int64(int64(v)) + } + if v, ok := group["subject_key"].(string); ok { + options.SubjectKey = aws.String(v) + } + } + } + + return &options +} + +func expandSAMLOptionsIdp(l []interface{}) *elasticsearch.SAMLIdp { + if len(l) == 0 { + return nil + } + + if l[0] == nil { + return &elasticsearch.SAMLIdp{} + } + + m := l[0].(map[string]interface{}) + + return &elasticsearch.SAMLIdp{ + EntityId: aws.String(m["entity_id"].(string)), + MetadataContent: aws.String(m["metadata_content"].(string)), + } +} + func flattenAdvancedSecurityOptions(advancedSecurityOptions *elasticsearch.AdvancedSecurityOptions) []map[string]interface{} { if advancedSecurityOptions == nil { return []map[string]interface{}{} @@ -58,6 +109,49 @@ func flattenAdvancedSecurityOptions(advancedSecurityOptions *elasticsearch.Advan return []map[string]interface{}{m} } +func flattenESSAMLOptions(d *schema.ResourceData, samlOptions *elasticsearch.SAMLOptionsOutput) []interface{} { + if samlOptions == nil { + return nil + } + + m := map[string]interface{}{ + "enabled": aws.BoolValue(samlOptions.Enabled), + "idp": flattenESSAMLIdpOptions(samlOptions.Idp), + } + + if samlOptions.RolesKey != nil { + m["roles_key"] = aws.StringValue(samlOptions.RolesKey) + } + if samlOptions.SessionTimeoutMinutes != nil { + m["session_timeout_minutes"] = aws.Int64Value(samlOptions.SessionTimeoutMinutes) + } + if samlOptions.SubjectKey != nil { + m["subject_key"] = aws.StringValue(samlOptions.SubjectKey) + } + + // samlOptions.master_backend_role and samlOptions.master_user_name will be added to the + // all_access role in kibana's security manager. These values cannot be read or + // modified by the elasticsearch API. So, we ignore it on read and let persist + // the value already in the state. + m["master_backend_role"] = d.Get("saml_options.0.master_backend_role").(string) + m["master_user_name"] = d.Get("saml_options.0.master_user_name").(string) + + return []interface{}{m} +} + +func flattenESSAMLIdpOptions(SAMLIdp *elasticsearch.SAMLIdp) []interface{} { + if SAMLIdp == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "entity_id": aws.StringValue(SAMLIdp.EntityId), + "metadata_content": aws.StringValue(SAMLIdp.MetadataContent), + } + + return []interface{}{m} +} + func getMasterUserOptions(d *schema.ResourceData) []interface{} { if v, ok := d.GetOk("advanced_security_options"); ok { options := v.([]interface{}) diff --git a/aws/provider.go b/aws/provider.go index 347b1ad8177..c9371087411 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -723,6 +723,7 @@ func Provider() *schema.Provider { "aws_elastic_beanstalk_environment": resourceAwsElasticBeanstalkEnvironment(), "aws_elasticsearch_domain": resourceAwsElasticSearchDomain(), "aws_elasticsearch_domain_policy": resourceAwsElasticSearchDomainPolicy(), + "aws_elasticsearch_domain_saml_options": resourceAwsElasticSearchDomainSAMLOptions(), "aws_elastictranscoder_pipeline": resourceAwsElasticTranscoderPipeline(), "aws_elastictranscoder_preset": resourceAwsElasticTranscoderPreset(), "aws_elb": resourceAwsElb(), diff --git a/aws/resource_aws_elasticsearch_domain_saml_options.go b/aws/resource_aws_elasticsearch_domain_saml_options.go new file mode 100644 index 00000000000..b9601a9b2c0 --- /dev/null +++ b/aws/resource_aws_elasticsearch_domain_saml_options.go @@ -0,0 +1,218 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + elasticsearch "github.com/aws/aws-sdk-go/service/elasticsearchservice" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceAwsElasticSearchDomainSAMLOptions() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsElasticSearchDomainSAMLOptionsPut, + Read: resourceAwsElasticSearchDomainSAMLOptionsRead, + Update: resourceAwsElasticSearchDomainSAMLOptionsPut, + Delete: resourceAwsElasticSearchDomainSAMLOptionsDelete, + + Schema: map[string]*schema.Schema{ + "domain_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "saml_options": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "idp": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "entity_id": { + Type: schema.TypeString, + Required: true, + }, + "metadata_content": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + "master_backend_role": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "master_user_name": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "roles_key": { + Type: schema.TypeString, + Optional: true, + }, + "session_timeout_minutes": { + Type: schema.TypeInt, + Optional: true, + Default: 60, + ValidateFunc: validation.IntBetween(1, 1440), + }, + "subject_key": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + } +} + +func resourceAwsElasticSearchDomainSAMLOptionsRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).esconn + + input := &elasticsearch.DescribeElasticsearchDomainInput{ + DomainName: aws.String(d.Get("domain_name").(string)), + } + + domain, err := conn.DescribeElasticsearchDomain(input) + + if err != nil { + if !d.IsNewResource() { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" { + log.Printf("[WARN] ElasticSearch Domain %q not found, removing from state", d.Id()) + d.SetId("") + return nil + } + } + return err + } + + log.Printf("[DEBUG] Received ElasticSearch domain: %s", domain) + + ds := domain.DomainStatus + options := ds.AdvancedSecurityOptions.SAMLOptions + + if err := d.Set("saml_options", flattenESSAMLOptions(d, options)); err != nil { + return fmt.Errorf("error setting saml_options for ElasticSearch Configuration: %w", err) + } + + return nil +} + +func resourceAwsElasticSearchDomainSAMLOptionsPut(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).esconn + + domainName := d.Get("domain_name").(string) + config := elasticsearch.AdvancedSecurityOptionsInput{} + config.SetSAMLOptions(expandESSAMLOptions(d.Get("saml_options").([]interface{}))) + + log.Printf("[DEBUG] Updating ElasticSearch domain SAML Options %s", config) + + _, err := conn.UpdateElasticsearchDomainConfig(&elasticsearch.UpdateElasticsearchDomainConfigInput{ + DomainName: aws.String(domainName), + AdvancedSecurityOptions: &config, + }) + + if err != nil { + return err + } + + d.SetId("esd-saml-options-" + domainName) + + input := &elasticsearch.DescribeElasticsearchDomainInput{ + DomainName: aws.String(d.Get("domain_name").(string)), + } + var out *elasticsearch.DescribeElasticsearchDomainOutput + err = resource.Retry(50*time.Minute, func() *resource.RetryError { + var err error + out, err = conn.DescribeElasticsearchDomain(input) + if err != nil { + return resource.NonRetryableError(err) + } + + if !*out.DomainStatus.Processing { + return nil + } + + return resource.RetryableError( + fmt.Errorf("%q: Timeout while waiting for changes to be processed", d.Id())) + }) + if isResourceTimeoutError(err) { + out, err = conn.DescribeElasticsearchDomain(input) + if err == nil && !*out.DomainStatus.Processing { + return nil + } + } + if err != nil { + return fmt.Errorf("Error updating Elasticsearch domain SAML Options: %s", err) + } + + return resourceAwsElasticSearchDomainSAMLOptionsRead(d, meta) +} + +func resourceAwsElasticSearchDomainSAMLOptionsDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).esconn + + domainName := d.Get("domain_name").(string) + config := elasticsearch.AdvancedSecurityOptionsInput{} + config.SetSAMLOptions(nil) + + _, err := conn.UpdateElasticsearchDomainConfig(&elasticsearch.UpdateElasticsearchDomainConfigInput{ + DomainName: aws.String(domainName), + AdvancedSecurityOptions: &config, + }) + if err != nil { + return err + } + + log.Printf("[DEBUG] Waiting for ElasticSearch domain SAML Options %q to be deleted", d.Get("domain_name").(string)) + + input := &elasticsearch.DescribeElasticsearchDomainInput{ + DomainName: aws.String(d.Get("domain_name").(string)), + } + var out *elasticsearch.DescribeElasticsearchDomainOutput + err = resource.Retry(60*time.Minute, func() *resource.RetryError { + var err error + out, err = conn.DescribeElasticsearchDomain(input) + if err != nil { + return resource.NonRetryableError(err) + } + + if !*out.DomainStatus.Processing { + return nil + } + + return resource.RetryableError( + fmt.Errorf("%q: Timeout while waiting for SAML Options to be deleted", d.Id())) + }) + if isResourceTimeoutError(err) { + out, err := conn.DescribeElasticsearchDomain(input) + if err == nil && !*out.DomainStatus.Processing { + return nil + } + } + if err != nil { + return fmt.Errorf("Error deleting Elasticsearch domain SAML Options: %s", err) + } + return nil +} diff --git a/aws/resource_aws_elasticsearch_domain_saml_options_test.go b/aws/resource_aws_elasticsearch_domain_saml_options_test.go new file mode 100644 index 00000000000..2899f0fb217 --- /dev/null +++ b/aws/resource_aws_elasticsearch_domain_saml_options_test.go @@ -0,0 +1,311 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + elasticsearch "github.com/aws/aws-sdk-go/service/elasticsearchservice" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccAWSElasticSearchDomainSAMLOptions_basic(t *testing.T) { + var domain elasticsearch.ElasticsearchDomainStatus + + rName := acctest.RandomWithPrefix("acc-test") + rUserName := acctest.RandomWithPrefix("es-master-user") + resourceName := "aws_elasticsearch_domain_saml_options.main" + esDomainResourceName := "aws_elasticsearch_domain.example" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticsearch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckESDomainSAMLOptionsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccESDomainSAMLOptionsConfig(rUserName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckESDomainExists(esDomainResourceName, &domain), + testAccCheckESDomainSAMLOptions(esDomainResourceName, resourceName), + resource.TestCheckResourceAttr(resourceName, "saml_options.#", "1"), + ), + }, + }, + }) +} + +func TestAccAWSElasticSearchDomainSAMLOptions_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("acc-test") + rUserName := acctest.RandomWithPrefix("es-master-user") + resourceName := "aws_elasticsearch_domain_saml_options.main" + esDomainResourceName := "aws_elasticsearch_domain.example" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticsearch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckESDomainSAMLOptionsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccESDomainSAMLOptionsConfig(rUserName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckESDomainSAMLOptions(esDomainResourceName, resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsElasticSearchDomainSAMLOptions(), resourceName), + ), + }, + }, + }) +} + +func TestAccAWSElasticSearchDomainSAMLOptions_disappears_Domain(t *testing.T) { + rName := acctest.RandomWithPrefix("acc-test") + rUserName := acctest.RandomWithPrefix("es-master-user") + resourceName := "aws_elasticsearch_domain_saml_options.main" + esDomainResourceName := "aws_elasticsearch_domain.example" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticsearch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckESDomainSAMLOptionsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccESDomainSAMLOptionsConfig(rUserName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckESDomainSAMLOptions(esDomainResourceName, resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsElasticSearchDomain(), esDomainResourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSElasticSearchDomainSAMLOptions_Update(t *testing.T) { + rName := acctest.RandomWithPrefix("acc-test") + rUserName := acctest.RandomWithPrefix("es-master-user") + resourceName := "aws_elasticsearch_domain_saml_options.main" + esDomainResourceName := "aws_elasticsearch_domain.example" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticsearch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckESDomainSAMLOptionsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccESDomainSAMLOptionsConfig(rUserName, rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "saml_options.#", "1"), + resource.TestCheckResourceAttr(resourceName, "saml_options.0.session_timeout_minutes", "60"), + testAccCheckESDomainSAMLOptions(esDomainResourceName, resourceName), + ), + }, + { + Config: testAccESDomainSAMLOptionsConfigUpdate(rUserName, rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "saml_options.#", "1"), + resource.TestCheckResourceAttr(resourceName, "saml_options.0.session_timeout_minutes", "180"), + testAccCheckESDomainSAMLOptions(esDomainResourceName, resourceName), + ), + }, + }, + }) +} + +func testAccCheckESDomainSAMLOptionsDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).esconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_elasticsearch_domain_saml_options" { + continue + } + + resp, err := conn.DescribeElasticsearchDomain(&elasticsearch.DescribeElasticsearchDomainInput{ + DomainName: aws.String(rs.Primary.Attributes["domain_name"]), + }) + + if err == nil { + return fmt.Errorf("Elasticsearch Domain still exists %s", resp) + } + + awsErr, ok := err.(awserr.Error) + if !ok { + return err + } + if awsErr.Code() != "ResourceNotFoundException" { + return err + } + + } + + return nil +} + +func testAccCheckESDomainSAMLOptionsDisappears(domainName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).esconn + + input := elasticsearch.AdvancedSecurityOptionsInput{} + input.SetSAMLOptions(nil) + _, err := conn.UpdateElasticsearchDomainConfig(&elasticsearch.UpdateElasticsearchDomainConfigInput{ + DomainName: aws.String(domainName), + AdvancedSecurityOptions: &input, + }) + + return err + } +} + +func testAccCheckESDomainSAMLOptions(esResource string, samlOptionsResource string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[esResource] + if !ok { + return fmt.Errorf("Not found: %s", esResource) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + options, ok := s.RootModule().Resources[samlOptionsResource] + if !ok { + return fmt.Errorf("Not found: %s", samlOptionsResource) + } + + conn := testAccProvider.Meta().(*AWSClient).esconn + _, err := conn.DescribeElasticsearchDomain(&elasticsearch.DescribeElasticsearchDomainInput{ + DomainName: aws.String(options.Primary.Attributes["domain_name"]), + }) + + return err + } +} + +func testAccESDomainSAMLOptionsConfig(userName string, domainName string) string { + return fmt.Sprintf(` +resource "aws_iam_user" "es_master_user" { + name = "%s" +} + +resource "aws_elasticsearch_domain" "example" { + domain_name = "%s" + elasticsearch_version = "7.10" + + cluster_config { + instance_type = "r5.large.elasticsearch" + } + + # Advanced security option must be enabled to configure SAML. + advanced_security_options { + enabled = true + internal_user_database_enabled = false + master_user_options { + master_user_arn = aws_iam_user.es_master_user.arn + } + } + + # You must enable node-to-node encryption to use advanced security options. + encrypt_at_rest { + enabled = true + } + + domain_endpoint_options { + enforce_https = true + tls_security_policy = "Policy-Min-TLS-1-2-2019-07" + } + + node_to_node_encryption { + enabled = true + } + + ebs_options { + ebs_enabled = true + volume_size = 10 + } +} + +resource "aws_elasticsearch_domain_saml_options" "main" { + domain_name = aws_elasticsearch_domain.example.domain_name + + saml_options { + # enabled = true + idp { + entity_id = "https://terraform-dev-ed.my.salesforce.com" + metadata_content = file("./test-fixtures/saml-metadata.xml") + } + # master_backend_role = "my-idp-group-or-role" + # master_user_name = "my-idp-user" + # roles_key = "optional-roles-key" + # session_timeout_minutes = 60 + # subject_key = "optional-subject-key" + } +} +`, userName, domainName) +} + +func testAccESDomainSAMLOptionsConfigUpdate(userName string, domainName string) string { + return fmt.Sprintf(` +resource "aws_iam_user" "es_master_user" { + name = "%s" +} + +resource "aws_elasticsearch_domain" "example" { + domain_name = "%s" + elasticsearch_version = "7.10" + + cluster_config { + instance_type = "r5.large.elasticsearch" + } + + # Advanced security option must be enabled to configure SAML. + advanced_security_options { + enabled = true + internal_user_database_enabled = false + master_user_options { + master_user_arn = aws_iam_user.es_master_user.arn + } + } + + # You must enable node-to-node encryption to use advanced security options. + encrypt_at_rest { + enabled = true + } + + domain_endpoint_options { + enforce_https = true + tls_security_policy = "Policy-Min-TLS-1-2-2019-07" + } + + node_to_node_encryption { + enabled = true + } + + ebs_options { + ebs_enabled = true + volume_size = 10 + } +} + +resource "aws_elasticsearch_domain_saml_options" "main" { + domain_name = aws_elasticsearch_domain.example.domain_name + + saml_options { + # enabled = true + idp { + entity_id = "https://terraform-dev-ed.my.salesforce.com" + metadata_content = file("./test-fixtures/saml-metadata.xml") + } + # master_backend_role = "my-idp-group-or-role" + # master_user_name = "my-idp-user" + # roles_key = "optional-roles-key" + session_timeout_minutes = 180 + # subject_key = "optional-subject-key" + } +} +`, userName, domainName) +} diff --git a/aws/resource_aws_elasticsearch_domain_test.go b/aws/resource_aws_elasticsearch_domain_test.go index f2baad9c4a9..5d332813673 100644 --- a/aws/resource_aws_elasticsearch_domain_test.go +++ b/aws/resource_aws_elasticsearch_domain_test.go @@ -11,7 +11,7 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" elasticsearch "github.com/aws/aws-sdk-go/service/elasticsearchservice" "github.com/aws/aws-sdk-go/service/iam" - "github.com/hashicorp/go-multierror" + multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -1457,6 +1457,18 @@ func testAccPreCheckIamServiceLinkedRoleEs(t *testing.T) { } } +func testAccCheckESDomainDisappears(domainName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).esconn + + _, err := conn.DeleteElasticsearchDomain(&elasticsearch.DeleteElasticsearchDomainInput{ + DomainName: aws.String(domainName), + }) + + return err + } +} + func testAccESDomainConfig(randInt int) string { return fmt.Sprintf(` resource "aws_elasticsearch_domain" "test" { From 27d209a7dbe1112d5066ad07cbc974ef88b7c5ad Mon Sep 17 00:00:00 2001 From: philof Date: Mon, 31 May 2021 11:56:55 -0400 Subject: [PATCH 2/6] Removes dead code --- ..._aws_elasticsearch_domain_saml_options_test.go | 15 --------------- aws/resource_aws_elasticsearch_domain_test.go | 12 ------------ 2 files changed, 27 deletions(-) diff --git a/aws/resource_aws_elasticsearch_domain_saml_options_test.go b/aws/resource_aws_elasticsearch_domain_saml_options_test.go index 2899f0fb217..56a814e3fea 100644 --- a/aws/resource_aws_elasticsearch_domain_saml_options_test.go +++ b/aws/resource_aws_elasticsearch_domain_saml_options_test.go @@ -146,21 +146,6 @@ func testAccCheckESDomainSAMLOptionsDestroy(s *terraform.State) error { return nil } -func testAccCheckESDomainSAMLOptionsDisappears(domainName string) resource.TestCheckFunc { - return func(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).esconn - - input := elasticsearch.AdvancedSecurityOptionsInput{} - input.SetSAMLOptions(nil) - _, err := conn.UpdateElasticsearchDomainConfig(&elasticsearch.UpdateElasticsearchDomainConfigInput{ - DomainName: aws.String(domainName), - AdvancedSecurityOptions: &input, - }) - - return err - } -} - func testAccCheckESDomainSAMLOptions(esResource string, samlOptionsResource string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[esResource] diff --git a/aws/resource_aws_elasticsearch_domain_test.go b/aws/resource_aws_elasticsearch_domain_test.go index 5d332813673..edb3fbf75d8 100644 --- a/aws/resource_aws_elasticsearch_domain_test.go +++ b/aws/resource_aws_elasticsearch_domain_test.go @@ -1457,18 +1457,6 @@ func testAccPreCheckIamServiceLinkedRoleEs(t *testing.T) { } } -func testAccCheckESDomainDisappears(domainName string) resource.TestCheckFunc { - return func(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).esconn - - _, err := conn.DeleteElasticsearchDomain(&elasticsearch.DeleteElasticsearchDomainInput{ - DomainName: aws.String(domainName), - }) - - return err - } -} - func testAccESDomainConfig(randInt int) string { return fmt.Sprintf(` resource "aws_elasticsearch_domain" "test" { From 0862f40c08d08a3dbf2bc35084ea6af06c3db28d Mon Sep 17 00:00:00 2001 From: philof Date: Mon, 31 May 2021 12:02:03 -0400 Subject: [PATCH 3/6] Applies terrafmt --- ..._elasticsearch_domain_saml_options_test.go | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/aws/resource_aws_elasticsearch_domain_saml_options_test.go b/aws/resource_aws_elasticsearch_domain_saml_options_test.go index 56a814e3fea..9469c3a7a93 100644 --- a/aws/resource_aws_elasticsearch_domain_saml_options_test.go +++ b/aws/resource_aws_elasticsearch_domain_saml_options_test.go @@ -218,16 +218,11 @@ resource "aws_elasticsearch_domain_saml_options" "main" { domain_name = aws_elasticsearch_domain.example.domain_name saml_options { - # enabled = true + enabled = true idp { - entity_id = "https://terraform-dev-ed.my.salesforce.com" - metadata_content = file("./test-fixtures/saml-metadata.xml") + entity_id = "https://terraform-dev-ed.my.salesforce.com" + metadata_content = file("./test-fixtures/saml-metadata.xml") } - # master_backend_role = "my-idp-group-or-role" - # master_user_name = "my-idp-user" - # roles_key = "optional-roles-key" - # session_timeout_minutes = 60 - # subject_key = "optional-subject-key" } } `, userName, domainName) @@ -280,16 +275,12 @@ resource "aws_elasticsearch_domain_saml_options" "main" { domain_name = aws_elasticsearch_domain.example.domain_name saml_options { - # enabled = true + enabled = true idp { - entity_id = "https://terraform-dev-ed.my.salesforce.com" - metadata_content = file("./test-fixtures/saml-metadata.xml") + entity_id = "https://terraform-dev-ed.my.salesforce.com" + metadata_content = file("./test-fixtures/saml-metadata.xml") } - # master_backend_role = "my-idp-group-or-role" - # master_user_name = "my-idp-user" - # roles_key = "optional-roles-key" - session_timeout_minutes = 180 - # subject_key = "optional-subject-key" + session_timeout_minutes = 180 } } `, userName, domainName) From 36cf3d752370ad747256f8ce3735312936033397 Mon Sep 17 00:00:00 2001 From: bill-rich Date: Tue, 22 Jun 2021 10:04:23 -0700 Subject: [PATCH 4/6] Add docs and adjust behavior --- aws/elasticsearch_domain_structure.go | 18 ++-- ...e_aws_elasticsearch_domain_saml_options.go | 41 +++++--- ..._elasticsearch_domain_saml_options_test.go | 93 +++++++++++++++++++ aws/resource_aws_elasticsearch_domain_test.go | 2 +- ...icsearch_domain_saml_options.html.markdown | 76 +++++++++++++++ 5 files changed, 206 insertions(+), 24 deletions(-) create mode 100644 website/docs/r/elasticsearch_domain_saml_options.html.markdown diff --git a/aws/elasticsearch_domain_structure.go b/aws/elasticsearch_domain_structure.go index 28b2c570c79..e6114eceb35 100644 --- a/aws/elasticsearch_domain_structure.go +++ b/aws/elasticsearch_domain_structure.go @@ -45,10 +45,14 @@ func expandAdvancedSecurityOptions(m []interface{}) *elasticsearch.AdvancedSecur } func expandESSAMLOptions(data []interface{}) *elasticsearch.SAMLOptionsInput { - if len(data) == 0 || data[0] == nil { + if len(data) == 0 { return nil } + if data[0] == nil { + return &elasticsearch.SAMLOptionsInput{} + } + options := elasticsearch.SAMLOptionsInput{} group := data[0].(map[string]interface{}) @@ -119,15 +123,9 @@ func flattenESSAMLOptions(d *schema.ResourceData, samlOptions *elasticsearch.SAM "idp": flattenESSAMLIdpOptions(samlOptions.Idp), } - if samlOptions.RolesKey != nil { - m["roles_key"] = aws.StringValue(samlOptions.RolesKey) - } - if samlOptions.SessionTimeoutMinutes != nil { - m["session_timeout_minutes"] = aws.Int64Value(samlOptions.SessionTimeoutMinutes) - } - if samlOptions.SubjectKey != nil { - m["subject_key"] = aws.StringValue(samlOptions.SubjectKey) - } + m["roles_key"] = aws.StringValue(samlOptions.RolesKey) + m["session_timeout_minutes"] = aws.Int64Value(samlOptions.SessionTimeoutMinutes) + m["subject_key"] = aws.StringValue(samlOptions.SubjectKey) // samlOptions.master_backend_role and samlOptions.master_user_name will be added to the // all_access role in kibana's security manager. These values cannot be read or diff --git a/aws/resource_aws_elasticsearch_domain_saml_options.go b/aws/resource_aws_elasticsearch_domain_saml_options.go index b9601a9b2c0..26179b74999 100644 --- a/aws/resource_aws_elasticsearch_domain_saml_options.go +++ b/aws/resource_aws_elasticsearch_domain_saml_options.go @@ -19,6 +19,12 @@ func resourceAwsElasticSearchDomainSAMLOptions() *schema.Resource { Read: resourceAwsElasticSearchDomainSAMLOptionsRead, Update: resourceAwsElasticSearchDomainSAMLOptionsPut, Delete: resourceAwsElasticSearchDomainSAMLOptionsDelete, + Importer: &schema.ResourceImporter{ + State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + d.Set("domain_name", d.Id()) + return []*schema.ResourceData{d}, nil + }, + }, Schema: map[string]*schema.Schema{ "domain_name": { @@ -71,14 +77,17 @@ func resourceAwsElasticSearchDomainSAMLOptions() *schema.Resource { Optional: true, }, "session_timeout_minutes": { - Type: schema.TypeInt, - Optional: true, - Default: 60, - ValidateFunc: validation.IntBetween(1, 1440), + Type: schema.TypeInt, + Optional: true, + Default: 60, + ValidateFunc: validation.IntBetween(1, 1440), + DiffSuppressFunc: elasticsearchDomainSamlOptionsDiffSupress, }, "subject_key": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + Default: "NameID", + DiffSuppressFunc: elasticsearchDomainSamlOptionsDiffSupress, }, }, }, @@ -86,6 +95,14 @@ func resourceAwsElasticSearchDomainSAMLOptions() *schema.Resource { }, } } +func elasticsearchDomainSamlOptionsDiffSupress(k, old, new string, d *schema.ResourceData) bool { + if v, ok := d.Get("saml_options").([]interface{}); ok && len(v) > 0 { + if enabled, ok := v[0].(map[string]interface{})["enabled"].(bool); ok && !enabled { + return true + } + } + return false +} func resourceAwsElasticSearchDomainSAMLOptionsRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).esconn @@ -97,12 +114,10 @@ func resourceAwsElasticSearchDomainSAMLOptionsRead(d *schema.ResourceData, meta domain, err := conn.DescribeElasticsearchDomain(input) if err != nil { - if !d.IsNewResource() { - if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" { - log.Printf("[WARN] ElasticSearch Domain %q not found, removing from state", d.Id()) - d.SetId("") - return nil - } + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" { + log.Printf("[WARN] ElasticSearch Domain %q not found, removing from state", d.Id()) + d.SetId("") + return nil } return err } @@ -137,7 +152,7 @@ func resourceAwsElasticSearchDomainSAMLOptionsPut(d *schema.ResourceData, meta i return err } - d.SetId("esd-saml-options-" + domainName) + d.SetId(domainName) input := &elasticsearch.DescribeElasticsearchDomainInput{ DomainName: aws.String(d.Get("domain_name").(string)), diff --git a/aws/resource_aws_elasticsearch_domain_saml_options_test.go b/aws/resource_aws_elasticsearch_domain_saml_options_test.go index 9469c3a7a93..abb9ff90ddb 100644 --- a/aws/resource_aws_elasticsearch_domain_saml_options_test.go +++ b/aws/resource_aws_elasticsearch_domain_saml_options_test.go @@ -32,8 +32,16 @@ func TestAccAWSElasticSearchDomainSAMLOptions_basic(t *testing.T) { testAccCheckESDomainExists(esDomainResourceName, &domain), testAccCheckESDomainSAMLOptions(esDomainResourceName, resourceName), resource.TestCheckResourceAttr(resourceName, "saml_options.#", "1"), + resource.TestCheckResourceAttr(resourceName, "saml_options.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "saml_options.0.idp.#", "1"), + resource.TestCheckResourceAttr(resourceName, "saml_options.0.idp.0.entity_id", "https://terraform-dev-ed.my.salesforce.com"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -117,6 +125,38 @@ func TestAccAWSElasticSearchDomainSAMLOptions_Update(t *testing.T) { }) } +func TestAccAWSElasticSearchDomainSAMLOptions_Disabled(t *testing.T) { + rName := acctest.RandomWithPrefix("acc-test") + rUserName := acctest.RandomWithPrefix("es-master-user") + resourceName := "aws_elasticsearch_domain_saml_options.main" + esDomainResourceName := "aws_elasticsearch_domain.example" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticsearch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckESDomainSAMLOptionsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccESDomainSAMLOptionsConfig(rUserName, rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "saml_options.#", "1"), + resource.TestCheckResourceAttr(resourceName, "saml_options.0.session_timeout_minutes", "60"), + testAccCheckESDomainSAMLOptions(esDomainResourceName, resourceName), + ), + }, + { + Config: testAccESDomainSAMLOptionsConfigDisabled(rUserName, rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "saml_options.#", "1"), + resource.TestCheckResourceAttr(resourceName, "saml_options.0.session_timeout_minutes", "0"), + testAccCheckESDomainSAMLOptions(esDomainResourceName, resourceName), + ), + }, + }, + }) +} + func testAccCheckESDomainSAMLOptionsDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).esconn @@ -285,3 +325,56 @@ resource "aws_elasticsearch_domain_saml_options" "main" { } `, userName, domainName) } + +func testAccESDomainSAMLOptionsConfigDisabled(userName string, domainName string) string { + return fmt.Sprintf(` +resource "aws_iam_user" "es_master_user" { + name = "%s" +} + +resource "aws_elasticsearch_domain" "example" { + domain_name = "%s" + elasticsearch_version = "7.10" + + cluster_config { + instance_type = "r5.large.elasticsearch" + } + + # Advanced security option must be enabled to configure SAML. + advanced_security_options { + enabled = true + internal_user_database_enabled = false + master_user_options { + master_user_arn = aws_iam_user.es_master_user.arn + } + } + + # You must enable node-to-node encryption to use advanced security options. + encrypt_at_rest { + enabled = true + } + + domain_endpoint_options { + enforce_https = true + tls_security_policy = "Policy-Min-TLS-1-2-2019-07" + } + + node_to_node_encryption { + enabled = true + } + + ebs_options { + ebs_enabled = true + volume_size = 10 + } +} + +resource "aws_elasticsearch_domain_saml_options" "main" { + domain_name = aws_elasticsearch_domain.example.domain_name + + saml_options { + enabled = false + } +} +`, userName, domainName) +} diff --git a/aws/resource_aws_elasticsearch_domain_test.go b/aws/resource_aws_elasticsearch_domain_test.go index edb3fbf75d8..f2baad9c4a9 100644 --- a/aws/resource_aws_elasticsearch_domain_test.go +++ b/aws/resource_aws_elasticsearch_domain_test.go @@ -11,7 +11,7 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" elasticsearch "github.com/aws/aws-sdk-go/service/elasticsearchservice" "github.com/aws/aws-sdk-go/service/iam" - multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" diff --git a/website/docs/r/elasticsearch_domain_saml_options.html.markdown b/website/docs/r/elasticsearch_domain_saml_options.html.markdown new file mode 100644 index 00000000000..321dae3e0e5 --- /dev/null +++ b/website/docs/r/elasticsearch_domain_saml_options.html.markdown @@ -0,0 +1,76 @@ +--- +subcategory: "ElasticSearch" +layout: "aws" +page_title: "AWS: aws_elasticsearch_domain_saml_options" +description: |- + Terraform resource for managing SAML authentication options for an AWS Elasticsearch Domain. +--- + +# Resource: aws_elasticsearch_domain_saml_options + +Manages SAML authentication options for an AWS Elasticsearch Domain. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_elasticsearch_domain" "example" { + domain_name = "example" + elasticsearch_version = "1.5" + + cluster_config { + instance_type = "r4.large.elasticsearch" + } + + snapshot_options { + automated_snapshot_start_hour = 23 + } + + tags = { + Domain = "TestDomain" + } +} + +saml_options { + enabled = true + idp { + entity_id = "https://example.com" + metadata_content = file("./saml-metadata.xml") + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `domain_name` - (Required) Name of the domain. + +The following arguments are optional: + +* `saml_options` - (Optional) The SAML authentication options for an AWS Elasticsearch Domain. + +### saml_options + +* `enabled` - (Required) Whether SAML authentication is enabled. +* `idp` - (Optional) Information from your identity provider. +* `master_backend_role` - (Optional) This backend role from the SAML IdP receives full permissions to the cluster, equivalent to a new master user. +* `master_user_name` - (Options) This username from the SAML IdP receives full permissions to the cluster, equivalent to a new master user. +* `roles_key` - (Optional) Element of the SAML assertion to use for backend roles. Default is roles. +* `session_timeout_minutes` - (Optional) Duration of a session in minutes after a user logs in. Default is 60. Maximum value is 1,440. +* `subject_key` - (Optional) Element of the SAML assertion to use for username. Default is NameID. + + +#### idp + +* `entity_id` - (Required) The unique Entity ID of the application in SAML Identity Provider. +* `metadata_content` - (Required) The Metadata of the SAML application in xml format. + +## Import + +Elasticsearch domains can be imported using the `domain_name`, e.g. + +``` +$ terraform import aws_elasticsearch_domain.example domain_name +``` From 1894ccf2f1ed79cc5b742a1fe00c71143e0fd431 Mon Sep 17 00:00:00 2001 From: bill-rich Date: Tue, 22 Jun 2021 10:13:20 -0700 Subject: [PATCH 5/6] Fix doc formatting --- ..._elasticsearch_domain_saml_options_test.go | 2 +- ...icsearch_domain_saml_options.html.markdown | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/aws/resource_aws_elasticsearch_domain_saml_options_test.go b/aws/resource_aws_elasticsearch_domain_saml_options_test.go index abb9ff90ddb..808a6b78c3c 100644 --- a/aws/resource_aws_elasticsearch_domain_saml_options_test.go +++ b/aws/resource_aws_elasticsearch_domain_saml_options_test.go @@ -373,7 +373,7 @@ resource "aws_elasticsearch_domain_saml_options" "main" { domain_name = aws_elasticsearch_domain.example.domain_name saml_options { - enabled = false + enabled = false } } `, userName, domainName) diff --git a/website/docs/r/elasticsearch_domain_saml_options.html.markdown b/website/docs/r/elasticsearch_domain_saml_options.html.markdown index 321dae3e0e5..003ef48be72 100644 --- a/website/docs/r/elasticsearch_domain_saml_options.html.markdown +++ b/website/docs/r/elasticsearch_domain_saml_options.html.markdown @@ -32,13 +32,16 @@ resource "aws_elasticsearch_domain" "example" { } } -saml_options { - enabled = true - idp { - entity_id = "https://example.com" - metadata_content = file("./saml-metadata.xml") - } -} +resource "aws_elasticsearch_domain_saml_options" "example" { + domain_name = aws_elasticsearch_domain.example.domain_name + saml_options { + enabled = true + idp { + entity_id = "https://example.com" + metadata_content = file("./saml-metadata.xml") + } + } +} ``` ## Argument Reference @@ -67,10 +70,16 @@ The following arguments are optional: * `entity_id` - (Required) The unique Entity ID of the application in SAML Identity Provider. * `metadata_content` - (Required) The Metadata of the SAML application in xml format. +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The name of the domain the SAML options are associated with. + ## Import Elasticsearch domains can be imported using the `domain_name`, e.g. ``` -$ terraform import aws_elasticsearch_domain.example domain_name +$ terraform import aws_elasticsearch_domain_saml_options.example domain_name ``` From 02a016dd7a15a78ca116e6a7c5a750c0bb4ffcf8 Mon Sep 17 00:00:00 2001 From: bill-rich Date: Tue, 22 Jun 2021 11:23:21 -0700 Subject: [PATCH 6/6] Add changelog entry --- .changelog/19497.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/19497.txt diff --git a/.changelog/19497.txt b/.changelog/19497.txt new file mode 100644 index 00000000000..4ccd259ca05 --- /dev/null +++ b/.changelog/19497.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_elasticsearch_domain_saml_options +```