From 13e3638dd67125c11fcdb91ef81c27cedbf718d8 Mon Sep 17 00:00:00 2001 From: Jeff Malnick Date: Fri, 10 Jul 2020 15:32:44 -0700 Subject: [PATCH] Add principles to roles (#12) * feat: add principle to role resource * docs: add principle usage to role resource --- go.mod | 2 +- go.sum | 9 + internal/provider/resource_role.go | 119 +++++++--- internal/provider/resource_role_test.go | 251 +++++++++++++++++++++ website/docs/r/resource_role.html.markdown | 22 +- 5 files changed, 373 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index ec9955b7..04526345 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk v1.14.0 github.com/hashicorp/terraform-svchost v0.0.0-20191119180714-d2e4933b9136 // indirect github.com/hashicorp/vault-plugin-secrets-ad v0.6.5 // indirect - github.com/hashicorp/watchtower v0.0.0-20200622202726-0c8385b42359 + github.com/hashicorp/watchtower v0.0.0-20200710194626-bcdcaffa6f4a github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/oklog/run v1.1.0 // indirect diff --git a/go.sum b/go.sum index cd5d27ac..52ee90f9 100644 --- a/go.sum +++ b/go.sum @@ -130,6 +130,9 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -320,6 +323,7 @@ github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-bindata/go-bindata/v3 v3.1.3/go.mod h1:1/zrpXsLD8YDIbhZRqXzm1Ghc7NhEvIN9+Z6R5/xH4I= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -558,6 +562,7 @@ github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyN github.com/hashicorp/consul/sdk v0.2.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.4.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= github.com/hashicorp/dbassert v0.0.0-20200602132714-30dfb78f0133/go.mod h1:+B5eZq7vXqN4Gxkgjm4ub1bwG6OTcYzFL0r+bMvWorg= +github.com/hashicorp/dbassert v0.0.0-20200602142727-a6709b436ad6 h1:Nejn3Kli0VXWf26vr9XQEGDjw5CELP8rxOwbG4ruFa8= github.com/hashicorp/dbassert v0.0.0-20200602142727-a6709b436ad6/go.mod h1:+B5eZq7vXqN4Gxkgjm4ub1bwG6OTcYzFL0r+bMvWorg= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -738,6 +743,10 @@ github.com/hashicorp/watchtower v0.0.0-20200609011200-5333482ff89f h1:wP70LU8w0L github.com/hashicorp/watchtower v0.0.0-20200609011200-5333482ff89f/go.mod h1:sMFje9U/WYZKIiEzmcuJfWb8i7Uqh+y0ET0Fck2HZk4= github.com/hashicorp/watchtower v0.0.0-20200622202726-0c8385b42359 h1:rDuxADQN0Mqqwm1moGQTWdjB+z929vmXH6KMcm43Voo= github.com/hashicorp/watchtower v0.0.0-20200622202726-0c8385b42359/go.mod h1:dkyM9Wf1hgp6XEQJhPO03SQ93XJ78KjUYzRJfp+Essc= +github.com/hashicorp/watchtower v0.0.0-20200710171123-5302e6d7fff8 h1:4938moh+beeh1UnYroktrHOsprhjjBf3+X1HdJP/45E= +github.com/hashicorp/watchtower v0.0.0-20200710171123-5302e6d7fff8/go.mod h1:y22wYAss3hsYe/BtE0p7xSH1NeZQp8UKhpbTA8tVOl0= +github.com/hashicorp/watchtower v0.0.0-20200710194626-bcdcaffa6f4a h1:Veaf8DQz7J074kP0NHP0pzWWDwsNicytPQwb8FMVAgM= +github.com/hashicorp/watchtower v0.0.0-20200710194626-bcdcaffa6f4a/go.mod h1:y22wYAss3hsYe/BtE0p7xSH1NeZQp8UKhpbTA8tVOl0= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= diff --git a/internal/provider/resource_role.go b/internal/provider/resource_role.go index 143b44b8..4e2d059e 100644 --- a/internal/provider/resource_role.go +++ b/internal/provider/resource_role.go @@ -11,6 +11,9 @@ import ( const ( roleNameKey = "name" roleDescriptionKey = "description" + roleUsersKey = "users" + roleGroupsKey = "groups" + roleGrantsKey = "grants" ) func resourceRole() *schema.Resource { @@ -28,6 +31,16 @@ func resourceRole() *schema.Resource { Type: schema.TypeString, Optional: true, }, + roleUsersKey: { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + roleGroupsKey: { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, }, } @@ -47,6 +60,18 @@ func convertRoleToResourceData(u *roles.Role, d *schema.ResourceData) error { } } + if u.UserIds != nil { + if err := d.Set(roleUsersKey, u.UserIds); err != nil { + return err + } + } + + if u.GroupIds != nil { + if err := d.Set(roleGroupsKey, u.GroupIds); err != nil { + return err + } + } + d.SetId(u.Id) return nil @@ -63,6 +88,18 @@ func convertResourceDataToRole(d *schema.ResourceData) *roles.Role { name := nameVal.(string) u.Name = &name } + if val, ok := d.GetOk(roleGroupsKey); ok { + groupIds := val.(*schema.Set).List() + for _, i := range groupIds { + u.GroupIds = append(u.GroupIds, i.(string)) + } + } + if val, ok := d.GetOk(roleUsersKey); ok { + userIds := val.(*schema.Set).List() + for _, i := range userIds { + u.UserIds = append(u.UserIds, i.(string)) + } + } if d.Id() != "" { u.Id = d.Id() @@ -80,17 +117,23 @@ func resourceRoleCreate(d *schema.ResourceData, meta interface{}) error { Client: client, } - u := convertResourceDataToRole(d) + r := convertResourceDataToRole(d) + users := r.UserIds + groups := r.GroupIds - u, apiErr, err := o.CreateRole(ctx, u) - if err != nil { - return fmt.Errorf("error calling new role: %s", err.Error()) - } - if apiErr != nil { - return fmt.Errorf("error creating role: %s", *apiErr.Message) + r, apiErr, err := o.CreateRole(ctx, r) + if apiErr != nil || err != nil { + return fmt.Errorf("error creating role:\n API Err: %v\n Err: %v\n", *apiErr.Message, err) } - d.SetId(u.Id) + if len(users) > 0 || len(groups) > 0 { + r, apiErr, err = r.SetPrincipals(ctx, groups, users) + if apiErr != nil || err != nil { + return fmt.Errorf("error setting principle on role:\n API Err: %+v\n Err: %+v\n", *apiErr.Message, err) + } + } + + d.SetId(r.Id) return nil } @@ -104,9 +147,9 @@ func resourceRoleRead(d *schema.ResourceData, meta interface{}) error { Client: client, } - u := convertResourceDataToRole(d) + r := convertResourceDataToRole(d) - u, apiErr, err := o.ReadRole(ctx, u) + r, apiErr, err := o.ReadRole(ctx, r) if err != nil { return fmt.Errorf("error reading role: %s", err.Error()) } @@ -114,7 +157,7 @@ func resourceRoleRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error reading role: %s", *apiErr.Message) } - return convertRoleToResourceData(u, d) + return convertRoleToResourceData(r, d) } func resourceRoleUpdate(d *schema.ResourceData, meta interface{}) error { @@ -126,27 +169,50 @@ func resourceRoleUpdate(d *schema.ResourceData, meta interface{}) error { Client: client, } - u := convertResourceDataToRole(d) + r := convertResourceDataToRole(d) if d.HasChange(roleNameKey) { n := d.Get(roleNameKey).(string) - u.Name = &n + r.Name = &n } if d.HasChange(roleDescriptionKey) { d := d.Get(roleDescriptionKey).(string) - u.Description = &d + r.Description = &d } - u, apiErr, err := o.UpdateRole(ctx, u) - if err != nil { - return err - } - if apiErr != nil { - return fmt.Errorf("error updating role: %s\n Invalid request fields: %v\n", *apiErr.Message, apiErr.Details.RequestFields) + r.GroupIds = nil + r.UserIds = nil + + r, apiErr, err := o.UpdateRole(ctx, r) + if apiErr != nil || err != nil { + return fmt.Errorf("error updating role:\n API Err: %v\n Err: %v\n", *apiErr.Message, err) } - return convertRoleToResourceData(u, d) + userIDs := []string{} + if d.HasChange(roleUsersKey) { + users := d.Get(roleUsersKey).(*schema.Set).List() + for _, user := range users { + userIDs = append(userIDs, user.(string)) + } + } + + groupIDs := []string{} + if d.HasChange(roleGroupsKey) { + groups := d.Get(roleGroupsKey).(*schema.Set).List() + for _, group := range groups { + groupIDs = append(groupIDs, group.(string)) + } + } + + if d.HasChange(roleGroupsKey) || d.HasChange(roleUsersKey) { + r, apiErr, err = r.SetPrincipals(ctx, groupIDs, userIDs) + if apiErr != nil || err != nil { + return fmt.Errorf("error updating principle on role:\n API Err: %v\n Err: %v\n", *apiErr.Message, err) + } + } + + return convertRoleToResourceData(r, d) } func resourceRoleDelete(d *schema.ResourceData, meta interface{}) error { @@ -158,14 +224,11 @@ func resourceRoleDelete(d *schema.ResourceData, meta interface{}) error { Client: client, } - u := convertResourceDataToRole(d) + r := convertResourceDataToRole(d) - _, apiErr, err := o.DeleteRole(ctx, u) - if err != nil { - return fmt.Errorf("error deleting role: %s", err.Error()) - } - if apiErr != nil { - return fmt.Errorf("error deleting role: %s", *apiErr.Message) + _, apiErr, err := o.DeleteRole(ctx, r) + if apiErr != nil || err != nil { + return fmt.Errorf("error deleting role:\n API Err: %v\n Err: %v\n", *apiErr.Message, err) } return nil diff --git a/internal/provider/resource_role_test.go b/internal/provider/resource_role_test.go index 7e16d532..9d5857d7 100644 --- a/internal/provider/resource_role_test.go +++ b/internal/provider/resource_role_test.go @@ -31,8 +31,130 @@ resource "watchtower_role" "foo" { name = "test" description = "%s" }`, fooRoleDescriptionUpdate) + + fooRoleWithUser = ` +resource "watchtower_user" "foo" { + name = "foo" +} + +resource "watchtower_role" "foo" { + name = "test" + description = "test description" + users = [watchtower_user.foo.id] +}` + + fooRoleWithUserUpdate = ` +resource "watchtower_user" "foo" { + name = "foo" +} + +resource "watchtower_user" "bar" { + name = "bar" +} + +resource "watchtower_role" "foo" { + name = "test" + description = "test description" + users = [watchtower_user.foo.id, watchtower_user.bar.id] +}` + + fooRoleWithGroups = ` +resource "watchtower_group" "foo" { + name = "foo" +} + +resource "watchtower_role" "foo" { + name = "test" + description = "test description" + groups = [watchtower_group.foo.id] +}` + + fooRoleWithGroupsUpdate = ` +resource "watchtower_group" "foo" { + name = "foo" +} + +resource "watchtower_group" "bar" { + name = "bar" +} + +resource "watchtower_role" "foo" { + name = "test" + description = "test description" + groups = [watchtower_group.foo.id, watchtower_group.bar.id] +}` ) +func TestAccRoleWithUsers(t *testing.T) { + tc := controller.NewTestController(t, controller.WithDefaultOrgId("o_0000000000")) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + CheckDestroy: testAccCheckRoleResourceDestroy(t), + Steps: []resource.TestStep{ + { + // test create + Config: testConfig(url, fooRoleWithUser), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleResourceExists("watchtower_role.foo"), + testAccCheckRoleResourceUsersSet("watchtower_role.foo", []string{"watchtower_user.foo"}), + testAccCheckUserResourceExists("watchtower_user.foo"), + resource.TestCheckResourceAttr("watchtower_role.foo", roleDescriptionKey, "test description"), + resource.TestCheckResourceAttr("watchtower_role.foo", roleNameKey, "test"), + resource.TestCheckResourceAttr("watchtower_user.foo", "name", "foo"), + ), + }, + { + // test update + Config: testConfig(url, fooRoleWithUserUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleResourceExists("watchtower_role.foo"), + testAccCheckUserResourceExists("watchtower_user.foo"), + testAccCheckUserResourceExists("watchtower_user.bar"), + testAccCheckRoleResourceUsersSet("watchtower_role.foo", []string{"watchtower_user.foo", "watchtower_user.bar"}), + ), + }, + }, + }) +} + +func TestAccRoleWithGroups(t *testing.T) { + tc := controller.NewTestController(t, controller.WithDefaultOrgId("o_0000000000")) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + CheckDestroy: testAccCheckRoleResourceDestroy(t), + Steps: []resource.TestStep{ + { + // test create + Config: testConfig(url, fooRoleWithGroups), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleResourceExists("watchtower_role.foo"), + testAccCheckRoleResourceGroupsSet("watchtower_role.foo", []string{"watchtower_group.foo"}), + testAccCheckUserResourceExists("watchtower_group.foo"), + resource.TestCheckResourceAttr("watchtower_role.foo", roleDescriptionKey, "test description"), + resource.TestCheckResourceAttr("watchtower_role.foo", roleNameKey, "test"), + resource.TestCheckResourceAttr("watchtower_group.foo", "name", "foo"), + ), + }, + { + // test update + Config: testConfig(url, fooRoleWithGroupsUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleResourceExists("watchtower_role.foo"), + testAccCheckUserResourceExists("watchtower_group.foo"), + testAccCheckUserResourceExists("watchtower_group.bar"), + testAccCheckRoleResourceGroupsSet("watchtower_role.foo", []string{"watchtower_group.foo", "watchtower_group.bar"}), + ), + }, + }, + }) +} + func TestAccRole(t *testing.T) { tc := controller.NewTestController(t, controller.WithDefaultOrgId("o_0000000000")) defer tc.Shutdown() @@ -139,6 +261,131 @@ func testAccCheckRoleResourceExists(name string) resource.TestCheckFunc { } } +func testAccCheckRoleResourceUsersSet(name string, users []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("role resource not found: %s", name) + } + + id := rs.Primary.ID + if id == "" { + return fmt.Errorf("role resource ID is not set") + } + + userIDs := []string{} + for _, userResourceName := range users { + ur, ok := s.RootModule().Resources[userResourceName] + if !ok { + return fmt.Errorf("user resource not found: %s", userResourceName) + } + + userID := ur.Primary.ID + if id == "" { + return fmt.Errorf("user resource ID not set") + } + + userIDs = append(userIDs, userID) + } + + md := testProvider.Meta().(*metaData) + + u := roles.Role{Id: id} + + o := &scopes.Organization{ + Client: md.client, + } + + r, _, err := o.ReadRole(md.ctx, &u) + if err != nil { + return fmt.Errorf("Got an error when reading role %q: %v", id, err) + } + + // for every user set as a principle on the role in the state, ensure + // each role in watchtower has the same setings + if len(r.UserIds) == 0 { + return fmt.Errorf("no users found in watchtower") + } + + for _, stateUser := range r.UserIds { + ok := false + for _, gotUser := range userIDs { + if gotUser == stateUser { + fmt.Printf("[Debug] user found in WT: %s, %s", gotUser, stateUser) + ok = true + } + } + if !ok { + return fmt.Errorf("user in state not set in watchtower: %s", stateUser) + } + } + + return nil + } +} + +func testAccCheckRoleResourceGroupsSet(name string, groups []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("role resource not found: %s", name) + } + + id := rs.Primary.ID + if id == "" { + return fmt.Errorf("role resource ID is not set") + } + + groupIDs := []string{} + for _, groupResourceName := range groups { + gr, ok := s.RootModule().Resources[groupResourceName] + if !ok { + return fmt.Errorf("group resource not found: %s", groupResourceName) + } + + groupID := gr.Primary.ID + if id == "" { + return fmt.Errorf("group resource ID not set") + } + + groupIDs = append(groupIDs, groupID) + } + + md := testProvider.Meta().(*metaData) + + u := roles.Role{Id: id} + + o := &scopes.Organization{ + Client: md.client, + } + + r, _, err := o.ReadRole(md.ctx, &u) + if err != nil { + return fmt.Errorf("Got an error when reading role %q: %v", id, err) + } + + // for every user set as a principle on the role in the state, ensure + // each role in watchtower has the same setings + if len(r.GroupIds) == 0 { + return fmt.Errorf("no groups found in watchtower") + } + + for _, stateGroup := range r.GroupIds { + ok := false + for _, gotGroup := range groupIDs { + if gotGroup == stateGroup { + fmt.Printf("[Debug] group found in WT: %s, %s", gotGroup, stateGroup) + ok = true + } + } + if !ok { + return fmt.Errorf("group in state not set in watchtower: %s", stateGroup) + } + } + + return nil + } +} func testAccCheckRoleResourceDestroy(t *testing.T) resource.TestCheckFunc { return func(s *terraform.State) error { md := testProvider.Meta().(*metaData) @@ -146,6 +393,10 @@ func testAccCheckRoleResourceDestroy(t *testing.T) resource.TestCheckFunc { for _, rs := range s.RootModule().Resources { switch rs.Type { + case "watchtower_user": + continue + case "watchtower_group": + continue case "watchtower_role": id := rs.Primary.ID diff --git a/website/docs/r/resource_role.html.markdown b/website/docs/r/resource_role.html.markdown index 603053e6..07ea7f5d 100644 --- a/website/docs/r/resource_role.html.markdown +++ b/website/docs/r/resource_role.html.markdown @@ -10,6 +10,7 @@ description: |- The role resource allows you to configure a Watchtower role. ## Example Usage +Basic usage: ```hcl resource "watchtower_role" "example" { @@ -18,9 +19,28 @@ resource "watchtower_role" "example" { } ``` +Usage with a user resource: + +```hcl +resource "watchtower_user" "foo" { + name = "User 1" +} + +resource "watchtower_user" "bar" { + name = "User 2" +} + +resource "watchtower_role" "example" { + name = "My role" + description = "My first role!" + users [watchtower_user.foo.id, watchtower_user.bar.id] +} +``` + ## Argument Reference The following arguments are optional: * `name` - The role name. Defaults to the resource name. * `description` - The role description. - +* `users` - A list of user resource ID's to add as principles on the role. +* `groups` - A list of group resource ID's to add as principles on the role.