From 54b3677a1e2066b55c47c540da37ca1f7c321683 Mon Sep 17 00:00:00 2001 From: Alex Kalyvitis Date: Fri, 13 Dec 2019 21:39:52 +0100 Subject: [PATCH] improve role and permission handling --- auth0/provider.go | 6 -- auth0/resource_auth0_role.go | 106 +++++++++--------- auth0/resource_auth0_role_test.go | 92 +++++++++------- auth0/resource_auth0_user.go | 173 ++++++++++++++++++++++-------- auth0/resource_auth0_user_test.go | 92 +++++++++++++++- auth0/resource_data.go | 30 ++++++ auth0/resource_data_test.go | 12 +++ example/email_template/main.tf | 2 +- example/role/main.tf | 11 +- example/user/main.tf | 6 ++ 10 files changed, 370 insertions(+), 160 deletions(-) diff --git a/auth0/provider.go b/auth0/provider.go index cc7832a9..84bd069f 100644 --- a/auth0/provider.go +++ b/auth0/provider.go @@ -2,9 +2,7 @@ package auth0 import ( "os" - "testing" - "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "gopkg.in/auth0.v2/management" ) @@ -57,10 +55,6 @@ func Provider() *schema.Provider { } } -func TestMain(m *testing.M) { - resource.TestMain(m) -} - func configure(data *schema.ResourceData) (interface{}, error) { domain := data.Get("domain").(string) diff --git a/auth0/resource_auth0_role.go b/auth0/resource_auth0_role.go index e70cecad..d912c243 100644 --- a/auth0/resource_auth0_role.go +++ b/auth0/resource_auth0_role.go @@ -36,24 +36,23 @@ func newRole() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, Optional: true, ForceNew: true, + Removed: `This field has been removed. Use "auth0_user.roles" instead`, }, "permissions": { - Type: schema.TypeList, + Type: schema.TypeSet, + Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, - Optional: true, - ForceNew: true, + Required: true, }, "resource_server_identifier": { Type: schema.TypeString, - Optional: true, - ForceNew: true, + Required: true, }, }, }, - Optional: true, }, }, } @@ -66,6 +65,7 @@ func createRole(d *schema.ResourceData, m interface{}) error { if err := api.Role.Create(c); err != nil { return err } + d.SetId(auth0.StringValue(c.ID)) // Enable partial state mode. Sub-resources can potentially cause partial // state. Therefore we must explicitly tell Terraform what is safe to @@ -73,33 +73,12 @@ func createRole(d *schema.ResourceData, m interface{}) error { // // See: https://www.terraform.io/docs/extend/writing-custom-providers.html d.Partial(true) - - if d.HasChange("user_ids") { - users := buildUsers(d) - if len(users) > 0 { - err := api.Role.AssignUsers(*c.ID, users...) - if err != nil { - return err - } - } - d.SetPartial("user_ids") - } - - if d.HasChange("permissions") { - permissions := buildPermissions(d) - if len(permissions) > 0 { - err := api.Role.AssociatePermissions(*c.ID, permissions...) - if err != nil { - return err - } - } - d.SetPartial("permissions") + if err := assignRolePermissions(d, m); err != nil { + return err } - // We succeeded, disable partial mode. This causes Terraform to save // all fields again. d.Partial(false) - d.SetId(auth0.StringValue(c.ID)) return readRole(d, m) } @@ -116,22 +95,10 @@ func readRole(d *schema.ResourceData, m interface{}) error { d.Set("name", c.Name) d.Set("description", c.Description) - users, err := api.Role.Users(d.Id()) - if err != nil { - return err - } - - userIDs := []string{} - for _, user := range users { - userIDs = append(userIDs, *user.ID) - } - d.Set("user_ids", userIDs) - permissions, err := api.Role.Permissions(d.Id()) if err != nil { return err } - d.Set("permissions", func() (m []map[string]interface{}) { for _, permission := range permissions { m = append(m, map[string]interface{}{ @@ -141,6 +108,7 @@ func readRole(d *schema.ResourceData, m interface{}) error { } return m }()) + return nil } @@ -151,6 +119,11 @@ func updateRole(d *schema.ResourceData, m interface{}) error { if err != nil { return err } + d.Partial(true) + if err := assignRolePermissions(d, m); err != nil { + return err + } + d.Partial(false) return readRole(d, m) } @@ -167,25 +140,44 @@ func buildRole(d *schema.ResourceData) *management.Role { } } -func buildUsers(d *schema.ResourceData) []*management.User { - var users []*management.User - for _, val := range Slice(d, "user_ids") { - userID, _ := val.(string) - users = append(users, &management.User{ - ID: &userID, +func assignRolePermissions(d *schema.ResourceData, m interface{}) error { + + add, rm := Diff(d, "permissions") + + var addPermissions []*management.Permission + for _, addPermission := range add { + permission := addPermission.(map[string]interface{}) + addPermissions = append(addPermissions, &management.Permission{ + Name: auth0.String(permission["name"].(string)), + ResourceServerIdentifier: auth0.String(permission["resource_server_identifier"].(string)), }) } - return users -} -func buildPermissions(d *schema.ResourceData) []*management.Permission { - var permissions []*management.Permission - for _, val := range Slice(d, "permissions") { - permission := val.(map[string]interface{}) - permissions = append(permissions, &management.Permission{ - Name: String(MapData(permission), "name"), - ResourceServerIdentifier: String(MapData(permission), "resource_server_identifier"), + var rmPermissions []*management.Permission + for _, rmPermission := range rm { + permission := rmPermission.(map[string]interface{}) + rmPermissions = append(rmPermissions, &management.Permission{ + Name: auth0.String(permission["name"].(string)), + ResourceServerIdentifier: auth0.String(permission["resource_server_identifier"].(string)), }) } - return permissions + + api := m.(*management.Management) + + if len(rmPermissions) > 0 { + err := api.Role.RemovePermissions(d.Id(), rmPermissions...) + if err != nil { + return err + } + } + + if len(addPermissions) > 0 { + err := api.Role.AssociatePermissions(d.Id(), addPermissions...) + if err != nil { + return err + } + } + + d.SetPartial("permissions") + return nil } diff --git a/auth0/resource_auth0_role_test.go b/auth0/resource_auth0_role_test.go index 0ba59b4d..48028469 100644 --- a/auth0/resource_auth0_role_test.go +++ b/auth0/resource_auth0_role_test.go @@ -15,60 +15,76 @@ func TestAccRole(t *testing.T) { }, Steps: []resource.TestStep{ { - Config: testAccRoleCreate, + Config: testAccRole_create, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("auth0_role.my_role", "name", "Application - Role Acceptance Test"), - resource.TestCheckResourceAttr("auth0_role.my_role", "description", "Test Applications Role Long Description"), + resource.TestCheckResourceAttr("auth0_role.the_one", "name", "The One - Role - Acceptance Test"), + resource.TestCheckResourceAttr("auth0_role.the_one", "description", "The One - Role - Acceptance Test"), + resource.TestCheckResourceAttr("auth0_role.the_one", "permissions.#", "1"), ), }, { - Config: testAccRoleUpdate, + Config: testAccRole_update, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("auth0_role.my_role", "description", "Test Applications Role Long Description And Then Some"), - resource.TestCheckResourceAttr("auth0_role.my_role", "user_ids.0", "auth0|neo"), - resource.TestCheckResourceAttr("auth0_role.my_role", "user_ids.1", "auth0|trinity"), + resource.TestCheckResourceAttr("auth0_role.the_one", "description", "The One who will bring peace - Role - Acceptance Test"), + resource.TestCheckResourceAttr("auth0_role.the_one", "permissions.#", "2"), ), }, }, }) } -const testAccRoleCreate = ` -provider "auth0" {} +const testAccRole_create = ` +provider auth0 {} -resource "auth0_role" "my_role" { - name = "Application - Role Acceptance Test" - description = "Test Applications Role Long Description" +resource auth0_resource_server matrix { + name = "The One - Resource Server - Acceptance Test" + identifier = "https://matrix.com/" + scopes { + value = "stop:bullets" + description = "Stop bullets" + } + scopes { + value = "bring:peace" + description = "Bring peace" + } + } + +resource auth0_role the_one { + name = "The One - Role - Acceptance Test" + description = "The One - Role - Acceptance Test" + permissions { + name = "stop:bullets" + resource_server_identifier = auth0_resource_server.matrix.identifier + } } ` -const testAccRoleUpdate = ` -provider "auth0" {} - -resource "auth0_user" "neo" { - connection_name = "Username-Password-Authentication" - email = "neo@matrix.com" - username = "neo" - nickname = "neo" - password = "IAmThe#1" - user_id = "neo" -} +const testAccRole_update = ` +provider auth0 {} -resource "auth0_user" "trinity" { - connection_name = "Username-Password-Authentication" - email = "trinity@matrix.com" - username = "trinity" - nickname = "trinity" - password = "TheM4trixH4$Y0u" - user_id = "trinity" -} +resource auth0_resource_server matrix { + name = "The One - Resource Server - Acceptance Test" + identifier = "https://matrix.com/" + scopes { + value = "stop:bullets" + description = "Create bars" + } + scopes { + value = "bring:peace" + description = "Bring peace" + } + } -resource "auth0_role" "my_role" { - name = "Application Role Acceptance Test" - description = "Test Applications Role Long Description And Then Some" - user_ids = [ - auth0_user.neo.id, - auth0_user.trinity.id - ] +resource auth0_role the_one { + name = "The One - Role - Acceptance Test" + description = "The One who will bring peace - Role - Acceptance Test" + permissions { + name = "stop:bullets" + resource_server_identifier = auth0_resource_server.matrix.identifier + } + permissions { + name = "bring:peace" + resource_server_identifier = auth0_resource_server.matrix.identifier + } } ` diff --git a/auth0/resource_auth0_user.go b/auth0/resource_auth0_user.go index 53ff5927..217fe72f 100644 --- a/auth0/resource_auth0_user.go +++ b/auth0/resource_auth0_user.go @@ -36,10 +36,6 @@ func newUser() *schema.Resource { Type: schema.TypeString, Required: true, }, - "email": { - Type: schema.TypeString, - Optional: true, - }, "username": { Type: schema.TypeString, Optional: true, @@ -53,16 +49,10 @@ func newUser() *schema.Resource { Optional: true, Sensitive: true, }, - "phone_number": { + "email": { Type: schema.TypeString, Optional: true, }, - "user_metadata": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.ValidateJsonString, - DiffSuppressFunc: structure.SuppressJsonDiff, - }, "email_verified": { Type: schema.TypeBool, Optional: true, @@ -71,16 +61,31 @@ func newUser() *schema.Resource { Type: schema.TypeBool, Optional: true, }, + "phone_number": { + Type: schema.TypeString, + Optional: true, + }, "phone_verified": { Type: schema.TypeBool, Optional: true, }, + "user_metadata": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.ValidateJsonString, + DiffSuppressFunc: structure.SuppressJsonDiff, + }, "app_metadata": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.ValidateJsonString, DiffSuppressFunc: structure.SuppressJsonDiff, }, + "roles": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, }, } } @@ -95,39 +100,76 @@ func readUser(d *schema.ResourceData, m interface{}) error { d.Set("user_id", u.ID) d.Set("username", u.Username) d.Set("nickname", u.Nickname) - d.Set("phone_number", u.PhoneNumber) + d.Set("email", u.Email) d.Set("email_verified", u.EmailVerified) - d.Set("phone_verified", u.PhoneVerified) d.Set("verify_email", u.VerifyEmail) - d.Set("email", u.Email) + d.Set("phone_number", u.PhoneNumber) + d.Set("phone_verified", u.PhoneVerified) - if userMeta, err := structure.FlattenJsonToString(u.UserMetadata); err == nil { - d.Set("user_metadata", userMeta) + userMeta, err := structure.FlattenJsonToString(u.UserMetadata) + if err != nil { + return err } + d.Set("user_metadata", userMeta) - if appMeta, err := structure.FlattenJsonToString(u.AppMetadata); err == nil { - d.Set("app_metadata", appMeta) + appMeta, err := structure.FlattenJsonToString(u.AppMetadata) + if err != nil { + return err } + d.Set("app_metadata", appMeta) + + roles, err := api.User.GetRoles(d.Id()) + if err != nil { + return err + } + d.Set("roles", func() (v []interface{}) { + for _, role := range roles { + v = append(v, auth0.StringValue(role.ID)) + } + return + }()) return nil } func createUser(d *schema.ResourceData, m interface{}) error { - u := buildUser(d) + u, err := buildUser(d) + if err != nil { + return err + } api := m.(*management.Management) if err := api.User.Create(u); err != nil { return err } d.SetId(*u.ID) + + d.Partial(true) + err = assignUserRoles(d, m) + if err != nil { + return err + } + d.Partial(false) + return readUser(d, m) } func updateUser(d *schema.ResourceData, m interface{}) error { - u := buildUser(d) + u, err := buildUser(d) + if err != nil { + return err + } api := m.(*management.Management) - if err := api.User.Update(d.Id(), u); err != nil { + if userHasChange(u) { + if err := api.User.Update(d.Id(), u); err != nil { + return err + } + } + d.Partial(true) + err = assignUserRoles(d, m) + if err != nil { return err } + d.Partial(false) return readUser(d, m) } @@ -136,32 +178,28 @@ func deleteUser(d *schema.ResourceData, m interface{}) error { return api.User.Delete(d.Id()) } -func buildUser(d *schema.ResourceData) *management.User { - u := &management.User{ - ID: String(d, "user_id"), - Connection: String(d, "connection_name"), - Username: String(d, "username"), - Nickname: String(d, "nickname"), - PhoneNumber: String(d, "phone_number"), - EmailVerified: Bool(d, "email_verified"), - VerifyEmail: Bool(d, "verify_email"), - PhoneVerified: Bool(d, "phone_verified"), - Email: String(d, "email"), - Password: String(d, "password"), - } - - if d.HasChange("user_metadata") { - userMeta, err := structure.ExpandJsonFromString(d.Get("user_metadata").(string)) - if err == nil { - u.UserMetadata = userMeta - } +func buildUser(d *schema.ResourceData) (u *management.User, err error) { + + u = new(management.User) + u.ID = String(d, "user_id") + u.Connection = String(d, "connection_name") + u.Username = String(d, "username") + u.Nickname = String(d, "nickname") + u.PhoneNumber = String(d, "phone_number") + u.EmailVerified = Bool(d, "email_verified") + u.VerifyEmail = Bool(d, "verify_email") + u.PhoneVerified = Bool(d, "phone_verified") + u.Email = String(d, "email") + u.Password = String(d, "password") + + u.UserMetadata, err = JSON(d, "user_metadata") + if err != nil { + return nil, err } - if d.HasChange("app_metadata") { - appMeta, err := structure.ExpandJsonFromString(d.Get("app_metadata").(string)) - if err == nil { - u.AppMetadata = appMeta - } + u.AppMetadata, err = JSON(d, "app_metadata") + if err != nil { + return nil, err } if u.Username != nil || u.Password != nil || u.EmailVerified != nil || u.PhoneVerified != nil { @@ -176,5 +214,48 @@ func buildUser(d *schema.ResourceData) *management.User { u.Connection = auth0.String(d.Get("connection_name").(string)) } - return u + return u, nil +} + +func assignUserRoles(d *schema.ResourceData, m interface{}) error { + + add, rm := Diff(d, "roles") + + var addRoles []*management.Role + for _, addRole := range add { + addRoles = append(addRoles, &management.Role{ + ID: auth0.String(addRole.(string)), + }) + } + + var rmRoles []*management.Role + for _, rmRole := range rm { + rmRoles = append(rmRoles, &management.Role{ + ID: auth0.String(rmRole.(string)), + }) + } + + api := m.(*management.Management) + + if len(rmRoles) > 0 { + err := api.User.RemoveRoles(d.Id(), rmRoles...) + if err != nil { + return err + } + } + + if len(addRoles) > 0 { + err := api.User.AssignRoles(d.Id(), addRoles...) + if err != nil { + return err + } + } + + d.SetPartial("roles") + return nil +} + +func userHasChange(u *management.User) bool { + // hacky but we need to tell if an empty json is sent to the api. + return u.String() != "{}" } diff --git a/auth0/resource_auth0_user_test.go b/auth0/resource_auth0_user_test.go index 4414d2ca..291f1a66 100644 --- a/auth0/resource_auth0_user_test.go +++ b/auth0/resource_auth0_user_test.go @@ -29,29 +29,44 @@ provider "auth0" {} resource "auth0_user" "user" {} ` -func TestAccUserCreateUser(t *testing.T) { +func TestAccUser(t *testing.T) { resource.Test(t, resource.TestCase{ Providers: map[string]terraform.ResourceProvider{ "auth0": Provider(), }, Steps: []resource.TestStep{ { - Config: testAccUserCreateUser, + Config: testAccUser_create, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("auth0_user.user", "user_id", "auth0|12345"), resource.TestCheckResourceAttr("auth0_user.user", "email", "test@test.com"), resource.TestCheckResourceAttr("auth0_user.user", "nickname", "testnick"), resource.TestCheckResourceAttr("auth0_user.user", "connection_name", "Username-Password-Authentication"), + resource.TestCheckResourceAttr("auth0_user.user", "roles.#", "0"), + ), + }, + { + Config: testAccUser_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("auth0_user.user", "roles.#", "2"), + resource.TestCheckResourceAttr("auth0_role.owner", "name", "owner"), + resource.TestCheckResourceAttr("auth0_role.admin", "name", "admin"), + ), + }, + { + Config: testAccUser_update_again, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("auth0_user.user", "roles.#", "1"), ), }, }, }) } -const testAccUserCreateUser = ` -provider "auth0" {} +const testAccUser_create = ` +provider auth0 {} -resource "auth0_user" "user" { +resource auth0_user user { connection_name = "Username-Password-Authentication" username = "test" user_id = "12345" @@ -72,3 +87,70 @@ EOF EOF } ` + +const testAccUser_update = ` +provider auth0 {} + +resource auth0_user user { + connection_name = "Username-Password-Authentication" + username = "test" + user_id = "12345" + email = "test@test.com" + password = "passpass$12$12" + nickname = "testnick" + roles = [ auth0_role.owner.id, auth0_role.admin.id ] + user_metadata = <