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..43566960661 --- /dev/null +++ b/aws/resource_aws_kms_grant.go @@ -0,0 +1,414 @@ +package aws + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/kms" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +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: validateAwsKmsGrantPrincipal, + }, + "operations": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateAwsKmsGrantOperation, + }, + Required: true, + ForceNew: true, + }, + "constraints": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "encryption_context_equals": { + Type: schema.TypeMap, + Optional: true, + Elem: schema.TypeString, + ConflictsWith: []string{"constraints.0.encryption_context_subset"}, + }, + "encryption_context_subset": { + Type: schema.TypeMap, + Optional: true, + Elem: schema.TypeString, + ConflictsWith: []string{"constraints.0.encryption_context_equals"}, + }, + }, + }, + }, + "retiring_principal": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateAwsKmsGrantPrincipal, + }, + "grant_creation_tokens": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + 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 + + input := kms.CreateGrantInput{ + GranteePrincipal: aws.String(d.Get("grantee_principal").(string)), + KeyId: aws.String(d.Get("key_id").(string)), + Operations: expandStringList(d.Get("operations").([]interface{})), + } + + if v, ok := d.GetOk("name"); ok { + input.Name = aws.String(v.(string)) + } + if v, ok := d.GetOk("constraints"); ok { + input.Constraints = expandKmsGrantConstraints(v.([]interface{})) + } + 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 = expandStringList(v.([]interface{})) + } + + log.Printf("[DEBUG]: Adding new KMS Grant: %s", input) + + var out *kms.CreateGrantOutput + + err := resource.Retry(2*time.Minute, func() *resource.RetryError { + var err error + + out, err = conn.CreateGrant(&input) + + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + // 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. TODO: Possibly change the aws_iam_role code? + if awsErr.Code() == "DependencyTimeoutException" || + awsErr.Code() == "InternalException" || + awsErr.Code() == "InvalidArnException" { + 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(*out.GrantId) + d.Set("grant_id", out.GrantId) + d.Set("grant_token", out.GrantToken) + + return err +} + +func resourceAwsKmsGrantRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).kmsconn + + grantId := d.Id() + keyId := d.Get("key_id").(string) + + log.Printf("[DEBUG] Looking for grant id: %s", grantId) + grant, err := findKmsGrantByIdWithRetry(conn, keyId, grantId) + + if err != nil { + return err + } + if grant != nil { + d.Set("grantee_principal", grant.GranteePrincipal) + d.Set("operations", grant.Operations) + if *grant.Name != "" { + d.Set("name", grant.Name) + } + if grant.Constraints != nil { + d.Set("constraints", flattenKmsGrantConstraints(grant.Constraints)) + } + if grant.RetiringPrincipal != nil { + d.Set("retiring_principal", grant.RetiringPrincipal) + } + } + + return err +} + +// Retiring grants requires special permissions (i.e. the +// caller to be root, retiree principal, or grantee principal with retire grant +// privileges). So just revoke grants. +func resourceAwsKmsGrantDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).kmsconn + + grantId := d.Get("grant_id").(string) + keyId := d.Get("key_id").(string) + input := kms.RevokeGrantInput{ + GrantId: aws.String(grantId), + KeyId: aws.String(keyId), + } + + log.Printf("[DEBUG] Revoking KMS grant: %s", grantId) + _, err := conn.RevokeGrant(&input) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Checking if grant is revoked: %s", grantId) + err = waitForKmsGrantToBeRevoked(conn, keyId, grantId) + + return err +} + +func resourceAwsKmsGrantExists(d *schema.ResourceData, meta interface{}) (bool, error) { + conn := meta.(*AWSClient).kmsconn + + grantId := d.Id() + keyId := d.Get("key_id").(string) + + log.Printf("[DEBUG] Looking for Grant: %s", grantId) + grant, err := findKmsGrantByIdWithRetry(conn, keyId, grantId) + + if err != nil { + if grant != nil { + return true, err + } + return false, err + } + if grant != nil { + return true, err + } + + return false, err +} + +func getKmsGrantById(grants []*kms.GrantListEntry, grantIdentifier string) *kms.GrantListEntry { + for idx := range grants { + if *grants[idx].GrantId == grantIdentifier { + return grants[idx] + } + } + + 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(2*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) + } + + // The grant sometimes contains principals that identified by their unique id: "AROAJYCVIVUZIMTXXXXX" + // instead of "arn:aws:..." + // This causes the grant to get into a bad state with the wrong principals that would cause resource re-creation after refresh + // https://docs.aws.amazon.com/kms/latest/APIReference/API_CreateGrant.html#API_CreateGrant_RequestSyntax + // states only ARNs are valid for both the grantee & retiring principals + // Confirmed with AWS that this is expected behavior. + // TODO: Handle upstream or here? + if grant != nil { + if !strings.HasPrefix(*grant.GranteePrincipal, "arn:aws") { + return resource.RetryableError( + fmt.Errorf("[DEBUG] The grantee principal is not resolving to an ARN: %s, retrying", *grant.GranteePrincipal)) + } + if grant.RetiringPrincipal != nil { + if !strings.HasPrefix(*grant.RetiringPrincipal, "arn:aws") { + return resource.RetryableError( + fmt.Errorf("[DEBUG] The retiring principal is not resolving to an ARN: %s, retrying", *grant.RetiringPrincipal)) + } + } + } + + return nil + }) + + return grant, err +} + +// Used by the tests as well +func waitForKmsGrantToBeRevoked(conn *kms.KMS, keyId string, grantId string) error { + err := resource.Retry(2*time.Minute, func() *resource.RetryError { + grant, err := findKmsGrantById(conn, keyId, grantId, nil) + if err != nil { + if _, ok := err.(KmsGrantMissingError); ok { + if grant == nil { + 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(2*time.Minute, func() *resource.RetryError { + out, err = conn.ListGrants(&input) + + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() == "NotFoundException" || + awsErr.Code() == "DependencyTimeoutException" || + awsErr.Code() == "InternalException" { + 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)) +} + +func expandKmsGrantConstraints(configured []interface{}) *kms.GrantConstraints { + if len(configured) < 1 { + return nil + } + + var constraint kms.GrantConstraints + + for _, raw := range configured { + data := raw.(map[string]interface{}) + if contextEq, ok := data["encryption_context_equals"]; ok { + constraint.EncryptionContextEquals = stringMapToPointers(contextEq.(map[string]interface{})) + } + if contextSub, ok := data["encryption_context_subset"]; ok { + constraint.SetEncryptionContextSubset(stringMapToPointers(contextSub.(map[string]interface{}))) + } + } + + return &constraint +} + +func flattenKmsGrantConstraints(constraint *kms.GrantConstraints) []interface{} { + if constraint == nil { + return []interface{}{} + } + + m := make(map[string]interface{}, 0) + + if constraint.EncryptionContextEquals != nil { + m["encryption_context_equals"] = pointersMapToStringList(constraint.EncryptionContextEquals) + } + if constraint.EncryptionContextSubset != nil { + m["encryption_context_subset"] = pointersMapToStringList(constraint.EncryptionContextSubset) + } + + return []interface{}{m} +} + +// Custom error, so we don't have to rely on +// the content of an error message +type KmsGrantMissingError struct { + msg string +} + +func (e KmsGrantMissingError) Error() string { + return e.msg +} + +func NewKmsGrantMissingError(msg string) KmsGrantMissingError { + return KmsGrantMissingError{ + msg: 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..843239c3c58 --- /dev/null +++ b/aws/resource_aws_kms_grant_test.go @@ -0,0 +1,238 @@ +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.0", "Encrypt"), + resource.TestCheckResourceAttr("aws_kms_grant.basic", "operations.1", "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("withConstraints", timestamp, "foo = \"bar\""), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSKmsGrantExists("aws_kms_grant.withConstraints"), + resource.TestCheckResourceAttr("aws_kms_grant.withConstraints", "name", "withConstraints"), + resource.TestCheckResourceAttr("aws_kms_grant.withConstraints", "constraints.0.encryption_context_equals.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, encryptionContextEq 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 { + encryption_context_equals { + %s + } + } +} +`, timestamp, staticAssumeRolePolicyString, rName, rName, rName, encryptionContextEq) +} + +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) +} + +var 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..cca9c40d044 100644 --- a/aws/validators.go +++ b/aws/validators.go @@ -1395,6 +1395,57 @@ func validateAwsKmsName(v interface{}, k string) (ws []string, es []error) { return } +func validateAwsKmsGrantOperation(v interface{}, k string) (ws []string, es []error) { + value := v.(string) + + validTypes := map[string]bool{ + "Decrypt": true, + "Encrypt": true, + "GenerateDataKey": true, + "GenerateDataKeyWithoutPlaintext": true, + "ReEncryptFrom": true, + "ReEncryptTo": true, + "CreateGrant": true, + "RetireGrant": true, + "DescribeKey": true, + } + + if _, ok := validTypes[value]; !ok { + es = append(es, fmt.Errorf( + "%s must be one of the following: [ Decrypt, Encrypt, GenerateDataKey, GenerateDataKeyWithoutPlaintext, ReEncryptFrom, ReEncryptTo, CreateGrant, RetireGrant, DescribeKey ]", k)) + + } + 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 validateAwsKmsGrantPrincipal(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 !strings.HasPrefix(value, "arn:aws") { + es = append(es, fmt.Errorf("%s is not an ARN, as it doesn't begin with arn:aws", k)) + } + + return +} + func validateCognitoIdentityPoolName(v interface{}, k string) (ws []string, errors []error) { val := v.(string) if !regexp.MustCompile("^[\\w _]+$").MatchString(val) { 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 + > + aws_kms_grant + + > aws_kms_key diff --git a/website/docs/r/kms_grant.html.markdown b/website/docs/r/kms_grant.html.markdown new file mode 100644 index 00000000000..448eb41f1bf --- /dev/null +++ b/website/docs/r/kms_grant.html.markdown @@ -0,0 +1,73 @@ +--- +layout: "aws" +page_title: "AWS: aws_kms_grant" +sidebar_current: "docs-aws-resource-kms-grant" +description: |- + Provides a resource-based access control mechanism for KMS Customer Master Keys. +--- + +# aws_kms_grant + +Provides a resource-based access control mechanism for a KMS customer master key. + +## Example Usage + +```hcl +resource "aws_kms_key" "a" {} + +resource "aws_iam_role" "a" { +name = "iam-role-for-grant" + + assume_role_policy = <