Skip to content

Commit

Permalink
Add auth0_organization_members resource
Browse files Browse the repository at this point in the history
  • Loading branch information
sergiught committed Jun 2, 2023
1 parent 92759d1 commit c0f0e51
Show file tree
Hide file tree
Showing 10 changed files with 2,989 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/resources/organization_member.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ description: |-

This resource is used to manage the assignment of members and their roles within an organization.

!> To prevent issues, avoid using this resource together with the `auth0_organization_members` resource.

## Example Usage

```terraform
Expand Down
60 changes: 60 additions & 0 deletions docs/resources/organization_members.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
page_title: "Resource: auth0_organization_members"
description: |-
This resource is used to manage members of an organization.
---

# Resource: auth0_organization_members

This resource is used to manage members of an organization.

!> To prevent issues, avoid using this resource together with the `auth0_organization_member` resource.

## Example Usage

```terraform
resource "auth0_user" "user_1" {
connection_name = "Username-Password-Authentication"
email = "{{.testName}}1@auth0.com"
password = "MyPass123$"
}
resource "auth0_user" "user_2" {
connection_name = "Username-Password-Authentication"
email = "{{.testName}}2@auth0.com"
password = "MyPass123$"
}
resource "auth0_organization" "my_org" {
name = "some-org-{{.testName}}"
display_name = "{{.testName}}"
}
resource "auth0_organization_members" "my_members" {
organization_id = auth0_organization.my_org.id
members = [auth0_user.user_1.id, auth0_user.user_2.id]
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `members` (Set of String) Add user ID(s) directly from the tenant to become members of the organization.
- `organization_id` (String) The ID of the organization to assign the member to.

### Read-Only

- `id` (String) The ID of this resource.

## Import

Import is supported using the following syntax:

```shell
# This resource can be imported by specifying the organization ID.
#
# Example:
terraform import auth0_organization_members.my_org_members "org_XXXXX"
```
4 changes: 4 additions & 0 deletions examples/resources/auth0_organization_members/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# This resource can be imported by specifying the organization ID.
#
# Example:
terraform import auth0_organization_members.my_org_members "org_XXXXX"
21 changes: 21 additions & 0 deletions examples/resources/auth0_organization_members/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
resource "auth0_user" "user_1" {
connection_name = "Username-Password-Authentication"
email = "{{.testName}}1@auth0.com"
password = "MyPass123$"
}

resource "auth0_user" "user_2" {
connection_name = "Username-Password-Authentication"
email = "{{.testName}}2@auth0.com"
password = "MyPass123$"
}

resource "auth0_organization" "my_org" {
name = "some-org-{{.testName}}"
display_name = "{{.testName}}"
}

resource "auth0_organization_members" "my_members" {
organization_id = auth0_organization.my_org.id
members = [auth0_user.user_1.id, auth0_user.user_2.id]
}
203 changes: 203 additions & 0 deletions internal/auth0/organization/resource_members.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package organization

import (
"context"
"fmt"
"net/http"

"github.com/auth0/go-auth0/management"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/auth0/terraform-provider-auth0/internal/config"
"github.com/auth0/terraform-provider-auth0/internal/value"
)

// NewMembersResource will return a new auth0_organization_members (1:many) resource.
func NewMembersResource() *schema.Resource {
return &schema.Resource{
Description: "This resource is used to manage members of an organization.",
CreateContext: createOrganizationMembers,
ReadContext: readOrganizationMembers,
UpdateContext: updateOrganizationMembers,
DeleteContext: deleteOrganizationMembers,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
"organization_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "The ID of the organization to assign the member to.",
},
"members": {
Type: schema.TypeSet,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Required: true,
Description: "Add user ID(s) directly from the tenant to become members of the organization.",
},
},
}
}

func createOrganizationMembers(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
api := meta.(*config.Config).GetAPI()
mutex := meta.(*config.Config).GetMutex()

organizationID := data.Get("organization_id").(string)

mutex.Lock(organizationID)
defer mutex.Unlock(organizationID)

alreadyMembers, err := api.Organization.Members(organizationID)
if err != nil {
if mErr, ok := err.(management.Error); ok && mErr.Status() == http.StatusNotFound {
data.SetId("")
return nil
}

return diag.FromErr(err)
}

data.SetId(organizationID)

membersToAdd := *value.Strings(data.GetRawConfig().GetAttr("members"))

if diagnostics := guardAgainstErasingUnwantedMembers(
organizationID,
alreadyMembers.Members,
membersToAdd,
); diagnostics.HasError() {
data.SetId("")
return diagnostics
}

if err := api.Organization.AddMembers(organizationID, membersToAdd); err != nil {
return diag.FromErr(err)
}

return readOrganizationMembers(ctx, data, meta)
}

func readOrganizationMembers(_ context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
api := meta.(*config.Config).GetAPI()

members, err := api.Organization.Members(data.Id())
if err != nil {
if mErr, ok := err.(management.Error); ok && mErr.Status() == http.StatusNotFound {
data.SetId("")
return nil
}

return diag.FromErr(err)
}

result := multierror.Append(
data.Set("organization_id", data.Id()),
data.Set("members", flattenOrganizationMembers(members.Members)),
)

return diag.FromErr(result.ErrorOrNil())
}

func updateOrganizationMembers(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
api := meta.(*config.Config).GetAPI()
mutex := meta.(*config.Config).GetMutex()

organizationID := data.Get("organization_id").(string)

mutex.Lock(organizationID)
defer mutex.Unlock(organizationID)

toAdd, toRemove := value.Difference(data, "members")

removeMembers := make([]string, 0)
for _, member := range toRemove {
removeMembers = append(removeMembers, member.(string))
}

if len(removeMembers) > 0 {
if err := api.Organization.DeleteMember(organizationID, removeMembers); err != nil {
if mErr, ok := err.(management.Error); ok && mErr.Status() == http.StatusNotFound {
data.SetId("")
return nil
}

return diag.FromErr(err)
}
}

addMembers := make([]string, 0)
for _, member := range toAdd {
addMembers = append(addMembers, member.(string))
}

if len(addMembers) > 0 {
if err := api.Organization.AddMembers(organizationID, addMembers); err != nil {
return diag.FromErr(err)
}
}

return readOrganizationMembers(ctx, data, meta)
}

func deleteOrganizationMembers(_ context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
api := meta.(*config.Config).GetAPI()
mutex := meta.(*config.Config).GetMutex()

organizationID := data.Get("organization_id").(string)
membersToRemove := value.Strings(data.GetRawState().GetAttr("members"))

mutex.Lock(organizationID)
defer mutex.Unlock(organizationID)

if err := api.Organization.DeleteMember(organizationID, *membersToRemove); err != nil {
if mErr, ok := err.(management.Error); ok && mErr.Status() == http.StatusNotFound {
data.SetId("")
return nil
}

return diag.FromErr(err)
}

return nil
}

func guardAgainstErasingUnwantedMembers(
organizationID string,
alreadyMembers []management.OrganizationMember,
membersToAdd []string,
) diag.Diagnostics {
if len(alreadyMembers) == 0 {
return nil
}

return diag.Diagnostics{
diag.Diagnostic{
Severity: diag.Error,
Summary: "Organization with non empty members",
Detail: cmp.Diff(membersToAdd, alreadyMembers) +
fmt.Sprintf("\nThe organization already has members attached to it. "+
"Import the resource instead in order to proceed with the changes. "+
"Run: 'terraform import auth0_organization_members.<given-name> %s'.", organizationID),
},
}
}

func flattenOrganizationMembers(members []management.OrganizationMember) []string {
if len(members) == 0 {
return nil
}

flattenedMembers := make([]string, 0)
for _, member := range members {
flattenedMembers = append(flattenedMembers, member.GetUserID())
}

return flattenedMembers
}
Loading

0 comments on commit c0f0e51

Please sign in to comment.