diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 3e517d71f11d..44082a59c16f 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -106,6 +106,7 @@ func Provider() terraform.ResourceProvider { "aws_iam_group_membership": resourceAwsIamGroupMembership(), "aws_iam_instance_profile": resourceAwsIamInstanceProfile(), "aws_iam_policy": resourceAwsIamPolicy(), + "aws_iam_policy_attachment": resourceAwsIamPolicyAttachment(), "aws_iam_role_policy": resourceAwsIamRolePolicy(), "aws_iam_role": resourceAwsIamRole(), "aws_iam_server_certificate": resourceAwsIAMServerCertificate(), diff --git a/builtin/providers/aws/resource_aws_iam_policy_attachment.go b/builtin/providers/aws/resource_aws_iam_policy_attachment.go new file mode 100644 index 000000000000..a9028c57cb6c --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_policy_attachment.go @@ -0,0 +1,328 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsIamPolicyAttachment() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsIamPolicyAttachmentCreate, + Read: resourceAwsIamPolicyAttachmentRead, + Update: resourceAwsIamPolicyAttachmentUpdate, + Delete: resourceAwsIamPolicyAttachmentDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "users": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "roles": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "groups": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "policy_arn": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsIamPolicyAttachmentCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).iamconn + + name := d.Get("name").(string) + arn := d.Get("policy_arn").(string) + users := expandStringList(d.Get("users").(*schema.Set).List()) + roles := expandStringList(d.Get("roles").(*schema.Set).List()) + groups := expandStringList(d.Get("groups").(*schema.Set).List()) + + if len(users) > 0 && len(roles) > 0 && len(groups) > 0 { + return fmt.Errorf("[WARN] No Users, Roles, or Groups specified for IAM Policy Attachment %s", name) + } else { + var userErr, roleErr, groupErr error + if users != nil { + userErr = attachPolicyToUsers(conn, users, arn) + } + if roles != nil { + roleErr = attachPolicyToRoles(conn, roles, arn) + } + if groups != nil { + groupErr = attachPolicyToGroups(conn, groups, arn) + } + if userErr != nil || roleErr != nil || groupErr != nil { + return composeErrors(fmt.Sprint("[WARN] Error attaching policy with IAM Policy Attachment ", name, ":"), userErr, roleErr, groupErr) + } + } + d.SetId(d.Get("name").(string)) + return resourceAwsIamPolicyAttachmentRead(d, meta) +} + +func resourceAwsIamPolicyAttachmentRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).iamconn + arn := d.Get("policy_arn").(string) + name := d.Get("name").(string) + + _, err := conn.GetPolicy(&iam.GetPolicyInput{ + PolicyARN: aws.String(arn), + }) + + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() == "NoSuchIdentity" { + d.SetId("") + return nil + } + } + return err + } + + policyEntities, err := conn.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{ + PolicyARN: aws.String(arn), + }) + + if err != nil { + return err + } + + ul := make([]string, 0, len(policyEntities.PolicyUsers)) + rl := make([]string, 0, len(policyEntities.PolicyRoles)) + gl := make([]string, 0, len(policyEntities.PolicyGroups)) + + for _, u := range policyEntities.PolicyUsers { + ul = append(ul, *u.UserName) + } + + for _, r := range policyEntities.PolicyRoles { + rl = append(rl, *r.RoleName) + } + + for _, g := range policyEntities.PolicyGroups { + gl = append(gl, *g.GroupName) + } + + userErr := d.Set("users", ul) + roleErr := d.Set("roles", rl) + groupErr := d.Set("groups", gl) + + if userErr != nil || roleErr != nil || groupErr != nil { + return composeErrors(fmt.Sprint("[WARN} Error setting user, role, or group list from IAM Policy Attachment ", name, ":"), userErr, roleErr, groupErr) + } + + return nil +} +func resourceAwsIamPolicyAttachmentUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).iamconn + name := d.Get("name").(string) + var userErr, roleErr, groupErr error + + if d.HasChange("users") { + userErr = updateUsers(conn, d, meta) + } + if d.HasChange("roles") { + roleErr = updateRoles(conn, d, meta) + } + if d.HasChange("groups") { + groupErr = updateGroups(conn, d, meta) + } + if userErr != nil || roleErr != nil || groupErr != nil { + return composeErrors(fmt.Sprint("[WARN] Error updating user, role, or group list from IAM Policy Attachment ", name, ":"), userErr, roleErr, groupErr) + } + return resourceAwsIamPolicyAttachmentRead(d, meta) +} + +func resourceAwsIamPolicyAttachmentDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).iamconn + name := d.Get("name").(string) + arn := d.Get("policy_arn").(string) + users := expandStringList(d.Get("users").(*schema.Set).List()) + roles := expandStringList(d.Get("roles").(*schema.Set).List()) + groups := expandStringList(d.Get("groups").(*schema.Set).List()) + + var userErr, roleErr, groupErr error + if len(users) != 0 { + userErr = detachPolicyFromUsers(conn, users, arn) + } + if len(roles) != 0 { + roleErr = detachPolicyFromRoles(conn, roles, arn) + } + if len(groups) != 0 { + groupErr = detachPolicyFromGroups(conn, groups, arn) + } + if userErr != nil || roleErr != nil || groupErr != nil { + return composeErrors(fmt.Sprint("[WARN] Error removing user, role, or group list from IAM Policy Detach ", name, ":"), userErr, roleErr, groupErr) + } + return nil +} + +func composeErrors(desc string, uErr error, rErr error, gErr error) error { + errMsg := fmt.Sprintf(desc) + errs := []error{uErr, rErr, gErr} + for _, e := range errs { + if e != nil { + errMsg = errMsg + "\n– " + e.Error() + } + } + return fmt.Errorf(errMsg) +} + +func attachPolicyToUsers(conn *iam.IAM, users []*string, arn string) error { + for _, u := range users { + _, err := conn.AttachUserPolicy(&iam.AttachUserPolicyInput{ + UserName: u, + PolicyARN: aws.String(arn), + }) + if err != nil { + return err + } + } + return nil +} +func attachPolicyToRoles(conn *iam.IAM, roles []*string, arn string) error { + for _, r := range roles { + _, err := conn.AttachRolePolicy(&iam.AttachRolePolicyInput{ + RoleName: r, + PolicyARN: aws.String(arn), + }) + if err != nil { + return err + } + } + return nil +} +func attachPolicyToGroups(conn *iam.IAM, groups []*string, arn string) error { + for _, g := range groups { + _, err := conn.AttachGroupPolicy(&iam.AttachGroupPolicyInput{ + GroupName: g, + PolicyARN: aws.String(arn), + }) + if err != nil { + return err + } + } + return nil +} +func updateUsers(conn *iam.IAM, d *schema.ResourceData, meta interface{}) error { + arn := d.Get("policy_arn").(string) + o, n := d.GetChange("users") + if o == nil { + o = new(schema.Set) + } + if n == nil { + n = new(schema.Set) + } + os := o.(*schema.Set) + ns := n.(*schema.Set) + remove := expandStringList(os.Difference(ns).List()) + add := expandStringList(ns.Difference(os).List()) + + if rErr := detachPolicyFromUsers(conn, remove, arn); rErr != nil { + return rErr + } + if aErr := attachPolicyToUsers(conn, add, arn); aErr != nil { + return aErr + } + return nil +} +func updateRoles(conn *iam.IAM, d *schema.ResourceData, meta interface{}) error { + arn := d.Get("policy_arn").(string) + o, n := d.GetChange("roles") + if o == nil { + o = new(schema.Set) + } + if n == nil { + n = new(schema.Set) + } + os := o.(*schema.Set) + ns := n.(*schema.Set) + remove := expandStringList(os.Difference(ns).List()) + add := expandStringList(ns.Difference(os).List()) + + if rErr := detachPolicyFromRoles(conn, remove, arn); rErr != nil { + return rErr + } + if aErr := attachPolicyToRoles(conn, add, arn); aErr != nil { + return aErr + } + return nil +} +func updateGroups(conn *iam.IAM, d *schema.ResourceData, meta interface{}) error { + arn := d.Get("policy_arn").(string) + o, n := d.GetChange("groups") + if o == nil { + o = new(schema.Set) + } + if n == nil { + n = new(schema.Set) + } + os := o.(*schema.Set) + ns := n.(*schema.Set) + remove := expandStringList(os.Difference(ns).List()) + add := expandStringList(ns.Difference(os).List()) + + if rErr := detachPolicyFromGroups(conn, remove, arn); rErr != nil { + return rErr + } + if aErr := attachPolicyToGroups(conn, add, arn); aErr != nil { + return aErr + } + return nil + +} +func detachPolicyFromUsers(conn *iam.IAM, users []*string, arn string) error { + for _, u := range users { + _, err := conn.DetachUserPolicy(&iam.DetachUserPolicyInput{ + UserName: u, + PolicyARN: aws.String(arn), + }) + if err != nil { + return err + } + } + return nil +} +func detachPolicyFromRoles(conn *iam.IAM, roles []*string, arn string) error { + for _, r := range roles { + _, err := conn.DetachRolePolicy(&iam.DetachRolePolicyInput{ + RoleName: r, + PolicyARN: aws.String(arn), + }) + if err != nil { + return err + } + } + return nil +} +func detachPolicyFromGroups(conn *iam.IAM, groups []*string, arn string) error { + for _, g := range groups { + _, err := conn.DetachGroupPolicy(&iam.DetachGroupPolicyInput{ + GroupName: g, + PolicyARN: aws.String(arn), + }) + if err != nil { + return err + } + } + return nil +} diff --git a/builtin/providers/aws/resource_aws_iam_policy_attachment_test.go b/builtin/providers/aws/resource_aws_iam_policy_attachment_test.go new file mode 100644 index 000000000000..3e6ef445d83b --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_policy_attachment_test.go @@ -0,0 +1,213 @@ +package aws + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "testing" +) + +func TestAccAWSPolicyAttachment_basic(t *testing.T) { + var out iam.ListEntitiesForPolicyOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSPolicyAttachmentDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSPolicyAttachConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSPolicyAttachmentExists("aws_iam_policy_attachment.test-attachment", 3, &out), + testAccCheckAWSPolicyAttachmentAttributes([]string{"test-user"}, []string{"test-role"}, []string{"test-group"}, &out), + ), + }, + resource.TestStep{ + Config: testAccAWSPolicyAttachConfigUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSPolicyAttachmentExists("aws_iam_policy_attachment.test-attachment", 6, &out), + testAccCheckAWSPolicyAttachmentAttributes([]string{"test-user3", "test-user3"}, []string{"test-role2", "test-role3"}, []string{"test-group2", "test-group3"}, &out), + ), + }, + }, + }) +} +func testAccCheckAWSPolicyAttachmentDestroy(s *terraform.State) error { + + return nil +} + +func testAccCheckAWSPolicyAttachmentExists(n string, c int64, out *iam.ListEntitiesForPolicyOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No policy name is set") + } + + conn := testAccProvider.Meta().(*AWSClient).iamconn + arn := rs.Primary.Attributes["policy_arn"] + + resp, err := conn.GetPolicy(&iam.GetPolicyInput{ + PolicyARN: aws.String(arn), + }) + if err != nil { + return fmt.Errorf("Error: Policy (%s) not found", n) + } + if c != *resp.Policy.AttachmentCount { + return fmt.Errorf("Error: Policy (%s) has wrong number of entities attached on initial creation", n) + } + resp2, err := conn.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{ + PolicyARN: aws.String(arn), + }) + if err != nil { + return fmt.Errorf("Error: Failed to get entities for Policy (%s)", arn) + } + + *out = *resp2 + return nil + } +} +func testAccCheckAWSPolicyAttachmentAttributes(users []string, roles []string, groups []string, out *iam.ListEntitiesForPolicyOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + uc := len(users) + rc := len(roles) + gc := len(groups) + + for _, u := range users { + for _, pu := range out.PolicyUsers { + if u == *pu.UserName { + uc-- + } + } + } + for _, r := range roles { + for _, pr := range out.PolicyRoles { + if r == *pr.RoleName { + rc-- + } + } + } + for _, g := range users { + for _, pg := range out.PolicyGroups { + if g == *pg.GroupName { + gc-- + } + } + } + if uc != 0 || rc != 0 || gc != 0 { + return fmt.Errorf("Error: Number of attached users, roles, or groups was incorrect:\n expected %d users and found %d\nexpected %d roles and found %d\nexpected %d groups and found %d", len(users), (len(users) - uc), len(roles), (len(roles) - rc), len(groups), (len(groups) - gc)) + } + return nil + } +} + +const testAccAWSPolicyAttachConfig = ` +resource "aws_iam_user" "user" { + name = "test-user" +} +resource "aws_iam_role" "role" { + name = "test-role" +} +resource "aws_iam_group" "group" { + name = "test-group" +} + +resource "aws_iam_policy" "policy" { + name = "test-policy" + description = "A test policy" + policy = <> aws_iam_policy + > + aws_iam_policy_attachment + > aws_iam_role