diff --git a/aws/provider.go b/aws/provider.go index 81a1dc110aa..7d050d86414 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -393,6 +393,7 @@ func Provider() terraform.ResourceProvider { "aws_kinesis_firehose_delivery_stream": resourceAwsKinesisFirehoseDeliveryStream(), "aws_kinesis_stream": resourceAwsKinesisStream(), "aws_kms_alias": resourceAwsKmsAlias(), + "aws_kms_grant": resourceAwsKmsGrant(), "aws_kms_key": resourceAwsKmsKey(), "aws_lambda_function": resourceAwsLambdaFunction(), "aws_lambda_event_source_mapping": resourceAwsLambdaEventSourceMapping(), diff --git a/aws/resource_aws_kms_grant.go b/aws/resource_aws_kms_grant.go new file mode 100644 index 00000000000..9d171dd5bf3 --- /dev/null +++ b/aws/resource_aws_kms_grant.go @@ -0,0 +1,540 @@ +package aws + +import ( + "bytes" + "fmt" + "log" + "sort" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/kms" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +func resourceAwsKmsGrant() *schema.Resource { + return &schema.Resource{ + // There is no API for updating/modifying grants, hence no Update + // Instead changes to most fields will force a new resource + Create: resourceAwsKmsGrantCreate, + Read: resourceAwsKmsGrantRead, + Delete: resourceAwsKmsGrantDelete, + Exists: resourceAwsKmsGrantExists, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateAwsKmsGrantName, + }, + "key_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "grantee_principal": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "operations": { + Type: schema.TypeSet, + Set: schema.HashString, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + kms.GrantOperationCreateGrant, + kms.GrantOperationDecrypt, + kms.GrantOperationDescribeKey, + kms.GrantOperationEncrypt, + kms.GrantOperationGenerateDataKey, + kms.GrantOperationGenerateDataKeyWithoutPlaintext, + kms.GrantOperationReEncryptFrom, + kms.GrantOperationReEncryptTo, + kms.GrantOperationRetireGrant, + }, false), + }, + Required: true, + ForceNew: true, + }, + "constraints": { + Type: schema.TypeSet, + Set: resourceKmsGrantConstraintsHash, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "encryption_context_equals": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Elem: schema.TypeString, + // ConflictsWith encryption_context_subset handled in Create, see kmsGrantConstraintsIsValid + }, + "encryption_context_subset": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Elem: schema.TypeString, + // ConflictsWith encryption_context_equals handled in Create, see kmsGrantConstraintsIsValid + }, + }, + }, + }, + "retiring_principal": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "grant_creation_tokens": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + ForceNew: true, + }, + "retire_on_delete": { + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + }, + "grant_id": { + Type: schema.TypeString, + Computed: true, + }, + "grant_token": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsKmsGrantCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).kmsconn + keyId := d.Get("key_id").(string) + + input := kms.CreateGrantInput{ + GranteePrincipal: aws.String(d.Get("grantee_principal").(string)), + KeyId: aws.String(keyId), + Operations: expandStringSet(d.Get("operations").(*schema.Set)), + } + + if v, ok := d.GetOk("name"); ok { + input.Name = aws.String(v.(string)) + } + if v, ok := d.GetOk("constraints"); ok { + if !kmsGrantConstraintsIsValid(v.(*schema.Set)) { + return fmt.Errorf("[ERROR] A grant constraint can't have both encryption_context_equals and encryption_context_subset set") + } + input.Constraints = expandKmsGrantConstraints(v.(*schema.Set)) + } + if v, ok := d.GetOk("retiring_principal"); ok { + input.RetiringPrincipal = aws.String(v.(string)) + } + if v, ok := d.GetOk("grant_creation_tokens"); ok { + input.GrantTokens = expandStringSet(v.(*schema.Set)) + } + + log.Printf("[DEBUG]: Adding new KMS Grant: %s", input) + + var out *kms.CreateGrantOutput + + err := resource.Retry(3*time.Minute, func() *resource.RetryError { + var err error + + out, err = conn.CreateGrant(&input) + + if err != nil { + // Error Codes: https://docs.aws.amazon.com/sdk-for-go/api/service/kms/#KMS.CreateGrant + // Under some circumstances a newly created IAM Role doesn't show up and causes + // an InvalidArnException to be thrown. + if isAWSErr(err, kms.ErrCodeDependencyTimeoutException, "") || + isAWSErr(err, kms.ErrCodeInternalException, "") || + isAWSErr(err, kms.ErrCodeInvalidArnException, "") { + return resource.RetryableError( + fmt.Errorf("[WARN] Error adding new KMS Grant for key: %s, retrying %s", + *input.KeyId, err)) + } + log.Printf("[ERROR] An error occured creating new AWS KMS Grant: %s", err) + return resource.NonRetryableError(err) + } + return nil + }) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Created new KMS Grant: %s", *out.GrantId) + d.SetId(fmt.Sprintf("%s:%s", keyId, *out.GrantId)) + d.Set("grant_id", out.GrantId) + d.Set("grant_token", out.GrantToken) + + return resourceAwsKmsGrantRead(d, meta) +} + +func resourceAwsKmsGrantRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).kmsconn + + keyId, grantId, err := decodeKmsGrantId(d.Id()) + if err != nil { + return err + } + + log.Printf("[DEBUG] Looking for grant id: %s", grantId) + grant, err := findKmsGrantByIdWithRetry(conn, keyId, grantId) + + if err != nil { + return err + } + + if grant == nil { + log.Printf("[WARN] %s KMS grant id not found for key id %s, removing from state file", grantId, keyId) + d.SetId("") + return nil + } + + // The grant sometimes contains principals that identified by their unique id: "AROAJYCVIVUZIMTXXXXX" + // instead of "arn:aws:...", in this case don't update the state file + if strings.HasPrefix(*grant.GranteePrincipal, "arn:aws") { + d.Set("grantee_principal", grant.GranteePrincipal) + } else { + log.Printf( + "[WARN] Unable to update grantee principal state %s for grant id %s for key id %s.", + *grant.GranteePrincipal, grantId, keyId) + } + + if grant.RetiringPrincipal != nil { + if strings.HasPrefix(*grant.RetiringPrincipal, "arn:aws") { + d.Set("retiring_principal", grant.RetiringPrincipal) + } else { + log.Printf( + "[WARN] Unable to update retiring principal state %s for grant id %s for key id %s", + *grant.RetiringPrincipal, grantId, keyId) + } + } + + if err := d.Set("operations", aws.StringValueSlice(grant.Operations)); err != nil { + log.Printf("[DEBUG] Error setting operations for grant %s with error %s", grantId, err) + } + if *grant.Name != "" { + d.Set("name", grant.Name) + } + if grant.Constraints != nil { + if err := d.Set("constraints", flattenKmsGrantConstraints(grant.Constraints)); err != nil { + log.Printf("[DEBUG] Error setting constraints for grant %s with error %s", grantId, err) + } + } + + return nil +} + +func resourceAwsKmsGrantDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).kmsconn + + keyId, grantId, decodeErr := decodeKmsGrantId(d.Id()) + if decodeErr != nil { + return decodeErr + } + doRetire := d.Get("retire_on_delete").(bool) + + var err error + if doRetire { + retireInput := kms.RetireGrantInput{ + GrantId: aws.String(grantId), + KeyId: aws.String(keyId), + } + + log.Printf("[DEBUG] Retiring KMS grant: %s", grantId) + _, err = conn.RetireGrant(&retireInput) + } else { + revokeInput := kms.RevokeGrantInput{ + GrantId: aws.String(grantId), + KeyId: aws.String(keyId), + } + + log.Printf("[DEBUG] Revoking KMS grant: %s", grantId) + _, err = conn.RevokeGrant(&revokeInput) + } + + if err != nil { + if isAWSErr(err, kms.ErrCodeNotFoundException, "") { + return nil + } + return err + } + + log.Printf("[DEBUG] Checking if grant is revoked: %s", grantId) + err = waitForKmsGrantToBeRevoked(conn, keyId, grantId) + + if err != nil { + return err + } + + return nil +} + +func resourceAwsKmsGrantExists(d *schema.ResourceData, meta interface{}) (bool, error) { + conn := meta.(*AWSClient).kmsconn + + keyId, grantId, err := decodeKmsGrantId(d.Id()) + if err != nil { + return false, err + } + + log.Printf("[DEBUG] Looking for Grant: %s", grantId) + grant, err := findKmsGrantByIdWithRetry(conn, keyId, grantId) + + if err != nil { + return true, err + } + if grant != nil { + return true, err + } + + return false, nil +} + +func getKmsGrantById(grants []*kms.GrantListEntry, grantIdentifier string) *kms.GrantListEntry { + for _, grant := range grants { + if *grant.GrantId == grantIdentifier { + return grant + } + } + + return nil +} + +/* +In the functions below it is not possible to use retryOnAwsCodes function, as there +is no describe grants call, so an error has to be created if the grant is or isn't returned +by the list grants call when expected. +*/ + +// NB: This function only retries the grant not being returned and some edge cases, while AWS Errors +// are handled by the findKmsGrantById function +func findKmsGrantByIdWithRetry(conn *kms.KMS, keyId string, grantId string) (*kms.GrantListEntry, error) { + var grant *kms.GrantListEntry + err := resource.Retry(3*time.Minute, func() *resource.RetryError { + var err error + grant, err = findKmsGrantById(conn, keyId, grantId, nil) + + if err != nil { + if serr, ok := err.(KmsGrantMissingError); ok { + // Force a retry if the grant should exist + return resource.RetryableError(serr) + } + + return resource.NonRetryableError(err) + } + + return nil + }) + + return grant, err +} + +// Used by the tests as well +func waitForKmsGrantToBeRevoked(conn *kms.KMS, keyId string, grantId string) error { + err := resource.Retry(3*time.Minute, func() *resource.RetryError { + grant, err := findKmsGrantById(conn, keyId, grantId, nil) + if err != nil { + if _, ok := err.(KmsGrantMissingError); ok { + return nil + } + } + + if grant != nil { + // Force a retry if the grant still exists + return resource.RetryableError( + fmt.Errorf("[DEBUG] Grant still exists while expected to be revoked, retyring revocation check: %s", *grant.GrantId)) + } + + return resource.NonRetryableError(err) + }) + + return err +} + +// The ListGrants API defaults to listing only 50 grants +// Use a marker to iterate over all grants in "pages" +// NB: This function only retries on AWS Errors +func findKmsGrantById(conn *kms.KMS, keyId string, grantId string, marker *string) (*kms.GrantListEntry, error) { + + input := kms.ListGrantsInput{ + KeyId: aws.String(keyId), + Limit: aws.Int64(int64(100)), + Marker: marker, + } + + var out *kms.ListGrantsResponse + var err error + var grant *kms.GrantListEntry + + err = resource.Retry(3*time.Minute, func() *resource.RetryError { + out, err = conn.ListGrants(&input) + + if err != nil { + if isAWSErr(err, kms.ErrCodeDependencyTimeoutException, "") || + isAWSErr(err, kms.ErrCodeInternalException, "") || + isAWSErr(err, kms.ErrCodeInvalidArnException, "") { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + + return nil + }) + + grant = getKmsGrantById(out.Grants, grantId) + if grant != nil { + return grant, nil + } + if *out.Truncated { + log.Printf("[DEBUG] KMS Grant list truncated, getting next page via marker: %s", *out.NextMarker) + return findKmsGrantById(conn, keyId, grantId, out.NextMarker) + } + + return nil, NewKmsGrantMissingError(fmt.Sprintf("[DEBUG] Grant %s not found for key id: %s", grantId, keyId)) +} + +// Can't have both constraint options set: +// ValidationException: More than one constraint supplied +// NB: set.List() returns an empty map if the constraint is not set, filter those out +// using len(v) > 0 +func kmsGrantConstraintsIsValid(constraints *schema.Set) bool { + constraintCount := 0 + for _, raw := range constraints.List() { + data := raw.(map[string]interface{}) + if v, ok := data["encryption_context_equals"].(map[string]interface{}); ok { + if len(v) > 0 { + constraintCount += 1 + } + } + if v, ok := data["encryption_context_subset"].(map[string]interface{}); ok { + if len(v) > 0 { + constraintCount += 1 + } + } + } + + if constraintCount > 1 { + return false + } + return true +} + +func expandKmsGrantConstraints(configured *schema.Set) *kms.GrantConstraints { + if len(configured.List()) < 1 { + return nil + } + + var constraint kms.GrantConstraints + + for _, raw := range configured.List() { + data := raw.(map[string]interface{}) + if contextEq, ok := data["encryption_context_equals"]; ok { + constraint.SetEncryptionContextEquals(stringMapToPointers(contextEq.(map[string]interface{}))) + } + if contextSub, ok := data["encryption_context_subset"]; ok { + constraint.SetEncryptionContextSubset(stringMapToPointers(contextSub.(map[string]interface{}))) + } + } + + return &constraint +} + +func sortStringMapKeys(m map[string]*string) []string { + keys := make([]string, 0) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + return keys +} + +// NB: For the constraint hash to be deterministic the order in which +// print the keys and values of the encryption context maps needs to be +// determistic, so sort them. +func sortedConcatStringMap(m map[string]*string, sep string) string { + var strList []string + mapKeys := sortStringMapKeys(m) + for _, key := range mapKeys { + strList = append(strList, key, *m[key]) + } + return strings.Join(strList, sep) +} + +// The hash needs to encapsulate what type of constraint it is +// as well as the keys and values of the constraint. +func resourceKmsGrantConstraintsHash(v interface{}) int { + var buf bytes.Buffer + m, castOk := v.(map[string]interface{}) + if !castOk { + return 0 + } + + if v, ok := m["encryption_context_equals"]; ok { + if len(v.(map[string]interface{})) > 0 { + buf.WriteString(fmt.Sprintf("encryption_context_equals-%s-", sortedConcatStringMap(stringMapToPointers(v.(map[string]interface{})), "-"))) + } + } + if v, ok := m["encryption_context_subset"]; ok { + if len(v.(map[string]interface{})) > 0 { + buf.WriteString(fmt.Sprintf("encryption_context_subset-%s-", sortedConcatStringMap(stringMapToPointers(v.(map[string]interface{})), "-"))) + } + } + + return hashcode.String(buf.String()) +} + +func flattenKmsGrantConstraints(constraint *kms.GrantConstraints) *schema.Set { + constraints := schema.NewSet(resourceKmsGrantConstraintsHash, []interface{}{}) + if constraint == nil { + return constraints + } + + m := make(map[string]interface{}, 0) + if constraint.EncryptionContextEquals != nil { + if len(constraint.EncryptionContextEquals) > 0 { + m["encryption_context_equals"] = pointersMapToStringList(constraint.EncryptionContextEquals) + } + } + if constraint.EncryptionContextSubset != nil { + if len(constraint.EncryptionContextSubset) > 0 { + m["encryption_context_subset"] = pointersMapToStringList(constraint.EncryptionContextSubset) + } + } + constraints.Add(m) + + return constraints +} + +func decodeKmsGrantId(id string) (string, string, error) { + parts := strings.Split(id, ":") + if len(parts) != 2 { + return "", "", fmt.Errorf("unexpected format of ID (%q), expected KeyID:GrantID", id) + } + return parts[0], parts[1], nil +} + +// Custom error, so we don't have to rely on +// the content of an error message +type KmsGrantMissingError string + +func (e KmsGrantMissingError) Error() string { + return e.Error() +} + +func NewKmsGrantMissingError(msg string) KmsGrantMissingError { + return KmsGrantMissingError(msg) +} diff --git a/aws/resource_aws_kms_grant_test.go b/aws/resource_aws_kms_grant_test.go new file mode 100644 index 00000000000..4cd8fbef8ef --- /dev/null +++ b/aws/resource_aws_kms_grant_test.go @@ -0,0 +1,254 @@ +package aws + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAWSKmsGrant_Basic(t *testing.T) { + timestamp := time.Now().Format(time.RFC1123) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSKmsGrantDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSKmsGrant_Basic("basic", timestamp, "\"Encrypt\", \"Decrypt\""), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSKmsGrantExists("aws_kms_grant.basic"), + resource.TestCheckResourceAttr("aws_kms_grant.basic", "name", "basic"), + resource.TestCheckResourceAttr("aws_kms_grant.basic", "operations.#", "2"), + resource.TestCheckResourceAttr("aws_kms_grant.basic", "operations.2238845196", "Encrypt"), + resource.TestCheckResourceAttr("aws_kms_grant.basic", "operations.1237510779", "Decrypt"), + resource.TestCheckResourceAttrSet("aws_kms_grant.basic", "grantee_principal"), + resource.TestCheckResourceAttrSet("aws_kms_grant.basic", "key_id"), + ), + }, + }, + }) +} + +func TestAWSKmsGrant_withConstraints(t *testing.T) { + timestamp := time.Now().Format(time.RFC1123) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSKmsGrantDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSKmsGrant_withConstraints("withConstraintsEq", timestamp, "encryption_context_equals", `foo = "bar" + baz = "kaz"`), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSKmsGrantExists("aws_kms_grant.withConstraintsEq"), + resource.TestCheckResourceAttr("aws_kms_grant.withConstraintsEq", "name", "withConstraintsEq"), + resource.TestCheckResourceAttr("aws_kms_grant.withConstraintsEq", "constraints.#", "1"), + resource.TestCheckResourceAttr("aws_kms_grant.withConstraintsEq", "constraints.449762259.encryption_context_equals.%", "2"), + resource.TestCheckResourceAttr("aws_kms_grant.withConstraintsEq", "constraints.449762259.encryption_context_equals.baz", "kaz"), + resource.TestCheckResourceAttr("aws_kms_grant.withConstraintsEq", "constraints.449762259.encryption_context_equals.foo", "bar"), + ), + }, + { + Config: testAccAWSKmsGrant_withConstraints("withConstraintsSub", timestamp, "encryption_context_subset", `foo = "bar" + baz = "kaz"`), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSKmsGrantExists("aws_kms_grant.withConstraintsSub"), + resource.TestCheckResourceAttr("aws_kms_grant.withConstraintsSub", "name", "withConstraintsSub"), + resource.TestCheckResourceAttr("aws_kms_grant.withConstraintsSub", "constraints.#", "1"), + resource.TestCheckResourceAttr("aws_kms_grant.withConstraintsSub", "constraints.2645649985.encryption_context_subset.%", "2"), + resource.TestCheckResourceAttr("aws_kms_grant.withConstraintsSub", "constraints.2645649985.encryption_context_subset.baz", "kaz"), + resource.TestCheckResourceAttr("aws_kms_grant.withConstraintsSub", "constraints.2645649985.encryption_context_subset.foo", "bar"), + ), + }, + }, + }) +} + +func TestAWSKmsGrant_withRetiringPrincipal(t *testing.T) { + timestamp := time.Now().Format(time.RFC1123) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSKmsGrantDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSKmsGrant_withRetiringPrincipal("withRetiringPrincipal", timestamp), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSKmsGrantExists("aws_kms_grant.withRetiringPrincipal"), + resource.TestCheckResourceAttrSet("aws_kms_grant.withRetiringPrincipal", "retiring_principal"), + ), + }, + }, + }) +} + +func TestAWSKmsGrant_bare(t *testing.T) { + timestamp := time.Now().Format(time.RFC1123) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSKmsGrantDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSKmsGrant_bare("bare", timestamp), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSKmsGrantExists("aws_kms_grant.bare"), + resource.TestCheckNoResourceAttr("aws_kms_grant.bare", "name"), + resource.TestCheckNoResourceAttr("aws_kms_grant.bare", "constraints.#"), + resource.TestCheckNoResourceAttr("aws_kms_grant.bare", "retiring_principal"), + ), + }, + }, + }) +} + +func testAccCheckAWSKmsGrantDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).kmsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_kms_grant" { + continue + } + + err := waitForKmsGrantToBeRevoked(conn, rs.Primary.Attributes["key_id"], rs.Primary.ID) + if err != nil { + return err + } + + return nil + } + + return nil +} + +func testAccCheckAWSKmsGrantExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + return nil + } +} + +func testAccAWSKmsGrant_Basic(rName string, timestamp string, operations string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "tf-acc-test-key" { + description = "Terraform acc test key %s" + deletion_window_in_days = 7 +} + +%s + +resource "aws_iam_role" "tf-acc-test-role" { + name = "tf-acc-test-kms-grant-role-%s" + path = "/service-role/" + assume_role_policy = "${data.aws_iam_policy_document.assumerole-policy-template.json}" +} + +resource "aws_kms_grant" "%s" { + name = "%s" + key_id = "${aws_kms_key.tf-acc-test-key.key_id}" + grantee_principal = "${aws_iam_role.tf-acc-test-role.arn}" + operations = [ %s ] +} +`, timestamp, staticAssumeRolePolicyString, rName, rName, rName, operations) +} + +func testAccAWSKmsGrant_withConstraints(rName string, timestamp string, constraintName string, encryptionContext string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "tf-acc-test-key" { + description = "Terraform acc test key %s" + deletion_window_in_days = 7 +} + +%s + +resource "aws_iam_role" "tf-acc-test-role" { + name = "tf-acc-test-kms-grant-role-%s" + path = "/service-role/" + assume_role_policy = "${data.aws_iam_policy_document.assumerole-policy-template.json}" +} + +resource "aws_kms_grant" "%s" { + name = "%s" + key_id = "${aws_kms_key.tf-acc-test-key.key_id}" + grantee_principal = "${aws_iam_role.tf-acc-test-role.arn}" + operations = [ "RetireGrant", "DescribeKey" ] + constraints { + %s { + %s + } + } +} +`, timestamp, staticAssumeRolePolicyString, rName, rName, rName, constraintName, encryptionContext) +} + +func testAccAWSKmsGrant_withRetiringPrincipal(rName string, timestamp string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "tf-acc-test-key" { + description = "Terraform acc test key %s" + deletion_window_in_days = 7 +} + +%s + +resource "aws_iam_role" "tf-acc-test-role" { + name = "tf-acc-test-kms-grant-role-%s" + path = "/service-role/" + assume_role_policy = "${data.aws_iam_policy_document.assumerole-policy-template.json}" +} + +resource "aws_kms_grant" "%s" { + name = "%s" + key_id = "${aws_kms_key.tf-acc-test-key.key_id}" + grantee_principal = "${aws_iam_role.tf-acc-test-role.arn}" + operations = [ "ReEncryptTo", "CreateGrant" ] + retiring_principal = "${aws_iam_role.tf-acc-test-role.arn}" +} +`, timestamp, staticAssumeRolePolicyString, rName, rName, rName) +} + +func testAccAWSKmsGrant_bare(rName string, timestamp string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "tf-acc-test-key" { + description = "Terraform acc test key %s" + deletion_window_in_days = 7 +} + +%s + +resource "aws_iam_role" "tf-acc-test-role" { + name = "tf-acc-test-kms-grant-role-%s" + path = "/service-role/" + assume_role_policy = "${data.aws_iam_policy_document.assumerole-policy-template.json}" +} + +resource "aws_kms_grant" "%s" { + key_id = "${aws_kms_key.tf-acc-test-key.key_id}" + grantee_principal = "${aws_iam_role.tf-acc-test-role.arn}" + operations = [ "ReEncryptTo", "CreateGrant" ] +} +`, timestamp, staticAssumeRolePolicyString, rName, rName) +} + +const staticAssumeRolePolicyString = ` +data "aws_iam_policy_document" "assumerole-policy-template" { + statement { + effect = "Allow" + actions = [ "sts:AssumeRole" ] + principals { + type = "Service" + identifiers = [ "ec2.amazonaws.com" ] + } + } +} +` diff --git a/aws/validators.go b/aws/validators.go index 109eb32fdf3..0565fc84538 100644 --- a/aws/validators.go +++ b/aws/validators.go @@ -1395,6 +1395,20 @@ func validateAwsKmsName(v interface{}, k string) (ws []string, es []error) { return } +func validateAwsKmsGrantName(v interface{}, k string) (ws []string, es []error) { + value := v.(string) + + if len(value) > 256 { + es = append(es, fmt.Errorf("%s can not be greater than 256 characters", k)) + } + + if !regexp.MustCompile(`^[a-zA-Z0-9:/_-]+$`).MatchString(value) { + es = append(es, fmt.Errorf("%s must only contain [a-zA-Z0-9:/_-]", k)) + } + + return +} + func validateCognitoIdentityPoolName(v interface{}, k string) (ws []string, errors []error) { val := v.(string) if !regexp.MustCompile("^[\\w _]+$").MatchString(val) { diff --git a/aws/validators_test.go b/aws/validators_test.go index 17a89aa1414..5a24c53afa2 100644 --- a/aws/validators_test.go +++ b/aws/validators_test.go @@ -2179,6 +2179,36 @@ func TestValidateAwsKmsName(t *testing.T) { } } +func TestValidateAwsKmsGrantName(t *testing.T) { + validValues := []string{ + "123", + "Abc", + "grant_1", + "grant:/-", + } + + for _, s := range validValues { + _, errors := validateAwsKmsGrantName(s, "name") + if len(errors) > 0 { + t.Fatalf("%q AWS KMS Grant Name should have been valid: %v", s, errors) + } + } + + invalidValues := []string{ + strings.Repeat("w", 257), + "grant.invalid", + ";", + "white space", + } + + for _, s := range invalidValues { + _, errors := validateAwsKmsGrantName(s, "name") + if len(errors) == 0 { + t.Fatalf("%q should not be a valid AWS KMS Grant Name", s) + } + } +} + func TestValidateCognitoIdentityPoolName(t *testing.T) { validValues := []string{ "123", diff --git a/website/aws.erb b/website/aws.erb index 6975f28532c..110dd14f335 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1107,6 +1107,10 @@ aws_kms_alias +