Skip to content

v1.7.0 Pre-Release #980

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

Merged
merged 4 commits into from
Dec 22, 2022
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
4 changes: 2 additions & 2 deletions .github_changelog_generator
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
future-release=v1.6.1
since-tag=v1.6.0
future-release=v1.7.0
since-tag=v1.6.1
date-format=%B %d, %Y
base=CHANGELOG.md
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/mwielbut/pointy v1.1.0
github.com/spf13/cast v1.5.0
github.com/terraform-providers/terraform-provider-aws v1.60.1-0.20210625132053-af2d5c0ad54f
go.mongodb.org/atlas v0.18.1-0.20221109142841-f9f8ebe7b9b9
go.mongodb.org/atlas v0.19.0
go.mongodb.org/realm v0.1.0
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,8 @@ go.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c/go.mod h1:xCI7ZzBfRuGgBXyXO6y
go.mongodb.org/atlas v0.12.0/go.mod h1:wVCnHcm/7/IfTjEB6K8K35PLG70yGz8BdkRwX0oK9/M=
go.mongodb.org/atlas v0.18.1-0.20221109142841-f9f8ebe7b9b9 h1:9m35o4kyRYjwbsIb/lPrjxJ6afPpn9zwOF5i3SIY5Lg=
go.mongodb.org/atlas v0.18.1-0.20221109142841-f9f8ebe7b9b9/go.mod h1:PFk1IGhiGjFXHGVspOK7i1U2nnPjK8wAjYwQf6FoVf4=
go.mongodb.org/atlas v0.19.0 h1:gvezG9d0KsSDaExEdTtcGqZHRvvVazzuEcBUpBXxmlg=
go.mongodb.org/atlas v0.19.0/go.mod h1:PFk1IGhiGjFXHGVspOK7i1U2nnPjK8wAjYwQf6FoVf4=
go.mongodb.org/realm v0.1.0 h1:zJiXyLaZrznQ+Pz947ziSrDKUep39DO4SfA0Fzx8M4M=
go.mongodb.org/realm v0.1.0/go.mod h1:4Vj6iy+Puo1TDERcoh4XZ+pjtwbOzPpzqy3Cwe8ZmDM=
go.mozilla.org/mozlog v0.0.0-20170222151521-4bb13139d403/go.mod h1:jHoPAGnDrCy6kaI2tAze5Prf0Nr0w/oNkROt2lw3n3o=
Expand Down
1 change: 1 addition & 0 deletions mongodbatlas/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Config struct {
PrivateKey string
BaseURL string
RealmBaseURL string
AssumeRole *AssumeRole
}

// MongoDBClient client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ func dataSourceMongoDBAtlasCloudBackupSnapshotsExportJobRead(ctx context.Context
ids := decodeStateID(d.Id())
projectID := ids["project_id"]
clusterName := ids["cluster_name"]
exportJobID := ids["export_job_id"]
exportID := ids["export_job_id"]

exportJob, _, err := conn.CloudProviderSnapshotExportJobs.Get(ctx, projectID, clusterName, exportJobID)
exportJob, _, err := conn.CloudProviderSnapshotExportJobs.Get(ctx, projectID, clusterName, exportID)
if err != nil {
return diag.Errorf("error getting snapshot export job information: %s", err)
}
Expand Down
297 changes: 296 additions & 1 deletion mongodbatlas/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,28 @@ package mongodbatlas
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"hash/crc32"
"log"
"os"
"reflect"
"regexp"
"sort"
"strconv"
"strings"

"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/mwielbut/pointy"
"github.com/spf13/cast"
matlas "go.mongodb.org/atlas/mongodbatlas"
Expand All @@ -24,6 +35,11 @@ var (
baseURL = ""
)

type SecretData struct {
PublicKey string `json:"public_key"`
PrivateKey string `json:"private_key"`
}

// Provider returns the provider to be use by the code.
func Provider() *schema.Provider {
provider := &schema.Provider{
Expand Down Expand Up @@ -67,6 +83,51 @@ func Provider() *schema.Provider {
Optional: true,
Description: "MongoDB Atlas Base URL default to gov",
},
"assume_role": assumeRoleSchema(),
"secret_name": {
Type: schema.TypeString,
Optional: true,
},
"region": {
Type: schema.TypeString,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"AWS_REGION",
"TF_VAR_AWS_REGION",
}, ""),
Optional: true,
},
"sts_endpoint": {
Type: schema.TypeString,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"STS_ENDPOINT",
"TF_VAR_STS_ENDPOINT",
}, ""),
Optional: true,
},
"aws_access_key_id": {
Type: schema.TypeString,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"AWS_ACCESS_KEY_ID",
"TF_VAR_AWS_ACCESS_KEY_ID",
}, ""),
Optional: true,
},
"aws_secret_access_key": {
Type: schema.TypeString,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"AWS_SECRET_ACCESS_KEY",
"TF_VAR_AWS_SECRET_ACCESS_KEY",
}, ""),
Optional: true,
},
"aws_session_token": {
Type: schema.TypeString,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"AWS_SESSION_TOKEN",
"TF_VAR_AWS_SESSION_TOKEN",
}, ""),
Optional: true,
},
},
DataSourcesMap: getDataSourcesMap(),
ResourcesMap: getResourcesMap(),
Expand Down Expand Up @@ -226,9 +287,79 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}
RealmBaseURL: d.Get("realm_base_url").(string),
}

if v, ok := d.GetOk("assume_role"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
config.AssumeRole = expandAssumeRole(v.([]interface{})[0].(map[string]interface{}))
secret := d.Get("secret_name").(string)
region := d.Get("region").(string)
awsAccessKeyID := d.Get("aws_access_key_id").(string)
awsSecretAccessKey := d.Get("aws_secret_access_key").(string)
awsSessionToken := d.Get("aws_session_token").(string)
endpoint := d.Get("sts_endpoint").(string)
config, _ = configureCredentialsSTS(&config, secret, region, awsAccessKeyID, awsSecretAccessKey, awsSessionToken, endpoint)
}

return config.NewClient(ctx)
}

func configureCredentialsSTS(config *Config, secret, region, awsAccessKeyID, awsSecretAccessKey, awsSessionToken, endpoint string) (Config, error) {
ep, _ := endpoints.GetSTSRegionalEndpoint("regional")
sess := session.Must(session.NewSession(&aws.Config{
Region: aws.String(region),
Credentials: credentials.NewStaticCredentials(awsAccessKeyID, awsSecretAccessKey, awsSessionToken),
STSRegionalEndpoint: ep,
Endpoint: &endpoint,
}))

creds := stscreds.NewCredentials(sess, config.AssumeRole.RoleARN)

_, _ = sess.Config.Credentials.Get()
_, _ = creds.Get()
secretString := secretsManagerGetSecretValue(sess, &aws.Config{Credentials: creds, Region: aws.String(region)}, secret)

var secretData SecretData
err := json.Unmarshal([]byte(secretString), &secretData)
if err != nil {
return *config, nil
}
config.PublicKey = secretData.PublicKey
config.PrivateKey = secretData.PrivateKey
return *config, nil
}

func secretsManagerGetSecretValue(sess *session.Session, creds *aws.Config, secret string) string {
svc := secretsmanager.New(sess, creds)
input := &secretsmanager.GetSecretValueInput{
SecretId: aws.String(secret),
VersionStage: aws.String("AWSCURRENT"),
}

result, err := svc.GetSecretValue(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case secretsmanager.ErrCodeResourceNotFoundException:
fmt.Println(secretsmanager.ErrCodeResourceNotFoundException, aerr.Error())
case secretsmanager.ErrCodeInvalidParameterException:
fmt.Println(secretsmanager.ErrCodeInvalidParameterException, aerr.Error())
case secretsmanager.ErrCodeInvalidRequestException:
fmt.Println(secretsmanager.ErrCodeInvalidRequestException, aerr.Error())
case secretsmanager.ErrCodeDecryptionFailure:
fmt.Println(secretsmanager.ErrCodeDecryptionFailure, aerr.Error())
case secretsmanager.ErrCodeInternalServiceError:
fmt.Println(secretsmanager.ErrCodeInternalServiceError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
fmt.Println(err.Error())
}
return ""
}

fmt.Println(result)
return *result.SecretString
}

func encodeStateID(values map[string]string) string {
encode := func(e string) string { return base64.StdEncoding.EncodeToString([]byte(e)) }
encodedValues := make([]string, 0)
Expand Down Expand Up @@ -391,3 +522,167 @@ func HashCodeString(s string) int {
// v == MinInt
return 0
}

// assumeRoleSchema From aws provider.go
func assumeRoleSchema() *schema.Schema {
return &schema.Schema{
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"duration": {
Type: schema.TypeString,
Optional: true,
Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.",
ValidateFunc: validAssumeRoleDuration,
ConflictsWith: []string{"assume_role.0.duration_seconds"},
},
"duration_seconds": {
Type: schema.TypeInt,
Optional: true,
Deprecated: "Use assume_role.duration instead",
Description: "The duration, in seconds, of the role session.",
ValidateFunc: validation.IntBetween(900, 43200),
ConflictsWith: []string{"assume_role.0.duration"},
},
"external_id": {
Type: schema.TypeString,
Optional: true,
Description: "A unique identifier that might be required when you assume a role in another account.",
ValidateFunc: validation.All(
validation.StringLenBetween(2, 1224),
validation.StringMatch(regexp.MustCompile(`[\w+=,.@:/\-]*`), ""),
),
},
"policy": {
Type: schema.TypeString,
Optional: true,
Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.",
ValidateFunc: validation.StringIsJSON,
},
"policy_arns": {
Type: schema.TypeSet,
Optional: true,
Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.",
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"role_arn": {
Type: schema.TypeString,
Optional: true,
Description: "Amazon Resource Name (ARN) of an IAM Role to assume prior to making API calls.",
},
"session_name": {
Type: schema.TypeString,
Optional: true,
Description: "An identifier for the assumed role session.",
ValidateFunc: validAssumeRoleSessionName,
},
"source_identity": {
Type: schema.TypeString,
Optional: true,
Description: "Source identity specified by the principal assuming the role.",
ValidateFunc: validAssumeRoleSourceIdentity,
},
"tags": {
Type: schema.TypeMap,
Optional: true,
Description: "Assume role session tags.",
Elem: &schema.Schema{Type: schema.TypeString},
},
"transitive_tag_keys": {
Type: schema.TypeSet,
Optional: true,
Description: "Assume role session tag keys to pass to any subsequent sessions.",
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
}
}

var validAssumeRoleSessionName = validation.All(
validation.StringLenBetween(2, 64),
validation.StringMatch(regexp.MustCompile(`[\w+=,.@\-]*`), ""),
)

var validAssumeRoleSourceIdentity = validation.All(
validation.StringLenBetween(2, 64),
validation.StringMatch(regexp.MustCompile(`[\w+=,.@\-]*`), ""),
)

// validAssumeRoleDuration validates a string can be parsed as a valid time.Duration
// and is within a minimum of 15 minutes and maximum of 12 hours
func validAssumeRoleDuration(v interface{}, k string) (ws []string, errors []error) {
duration, err := time.ParseDuration(v.(string))

if err != nil {
errors = append(errors, fmt.Errorf("%q cannot be parsed as a duration: %w", k, err))
return
}

if duration.Minutes() < 15 || duration.Hours() > 12 {
errors = append(errors, fmt.Errorf("duration %q must be between 15 minutes (15m) and 12 hours (12h), inclusive", k))
}

return
}

type AssumeRole struct {
RoleARN string
Duration time.Duration
ExternalID string
Policy string
PolicyARNs []string
SessionName string
SourceIdentity string
Tags map[string]string
TransitiveTagKeys []string
}

func expandAssumeRole(tfMap map[string]interface{}) *AssumeRole {
if tfMap == nil {
return nil
}

assumeRole := AssumeRole{}

if v, ok := tfMap["duration"].(string); ok && v != "" {
duration, _ := time.ParseDuration(v)
assumeRole.Duration = duration
} else if v, ok := tfMap["duration_seconds"].(int); ok && v != 0 {
assumeRole.Duration = time.Duration(v) * time.Second
}

if v, ok := tfMap["external_id"].(string); ok && v != "" {
assumeRole.ExternalID = v
}

if v, ok := tfMap["policy"].(string); ok && v != "" {
assumeRole.Policy = v
}

if v, ok := tfMap["policy_arns"].(*schema.Set); ok && v.Len() > 0 {
assumeRole.PolicyARNs = expandStringList(v.List())
}

if v, ok := tfMap["role_arn"].(string); ok && v != "" {
assumeRole.RoleARN = v
}

if v, ok := tfMap["session_name"].(string); ok && v != "" {
assumeRole.SessionName = v
}

if v, ok := tfMap["source_identity"].(string); ok && v != "" {
assumeRole.SourceIdentity = v
}

if v, ok := tfMap["transitive_tag_keys"].(*schema.Set); ok && v.Len() > 0 {
assumeRole.TransitiveTagKeys = expandStringList(v.List())
}

return &assumeRole
}
Loading