Skip to content

Commit

Permalink
Merge pull request #19497 from philof/r/elasticsearch_domain_saml_opt…
Browse files Browse the repository at this point in the history
…ions

r/aws_elasticsearch_domain_saml_options: New resource
  • Loading branch information
bill-rich authored Jun 22, 2021
2 parents 21ce53d + 02a016d commit 694895f
Show file tree
Hide file tree
Showing 6 changed files with 794 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/19497.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_elasticsearch_domain_saml_options
```
92 changes: 92 additions & 0 deletions aws/elasticsearch_domain_structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,61 @@ func expandAdvancedSecurityOptions(m []interface{}) *elasticsearch.AdvancedSecur
return &config
}

func expandESSAMLOptions(data []interface{}) *elasticsearch.SAMLOptionsInput {
if len(data) == 0 {
return nil
}

if data[0] == nil {
return &elasticsearch.SAMLOptionsInput{}
}

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{}{}
Expand All @@ -58,6 +113,43 @@ 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),
}

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
// 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{})
Expand Down
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,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(),
Expand Down
233 changes: 233 additions & 0 deletions aws/resource_aws_elasticsearch_domain_saml_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
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,
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": {
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),
DiffSuppressFunc: elasticsearchDomainSamlOptionsDiffSupress,
},
"subject_key": {
Type: schema.TypeString,
Optional: true,
Default: "NameID",
DiffSuppressFunc: elasticsearchDomainSamlOptionsDiffSupress,
},
},
},
},
},
}
}
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

input := &elasticsearch.DescribeElasticsearchDomainInput{
DomainName: aws.String(d.Get("domain_name").(string)),
}

domain, err := conn.DescribeElasticsearchDomain(input)

if err != 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
}

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(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
}
Loading

0 comments on commit 694895f

Please sign in to comment.