Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DXCDT-438: Add auth0_organization_connections resource #610

Merged
merged 3 commits into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/resources/organization_connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ description: |-

With this resource, you can manage enabled connections on an organization.

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

## Example Usage

```terraform
Expand Down
78 changes: 78 additions & 0 deletions docs/resources/organization_connections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
page_title: "Resource: auth0_organization_connections"
description: |-
With this resource, you can manage enabled connections on an organization.
---

# Resource: auth0_organization_connections

With this resource, you can manage enabled connections on an organization.

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

## Example Usage

```terraform
resource "auth0_connection" "my_connection-1" {
name = "My Connection 1"
strategy = "auth0"
}
resource "auth0_connection" "my_connection-2" {
name = "My Connection 2"
strategy = "auth0"
}
resource "auth0_organization" "my_organization" {
name = "my-organization"
display_name = "My Organization"
}
resource "auth0_organization_connections" "one-to-many" {
organization_id = auth0_organization.my_organization.id
enabled_connections {
connection_id = auth0_connection.my_connection-1.id
assign_membership_on_login = true
}
enabled_connections {
connection_id = auth0_connection.my_connection-2.id
assign_membership_on_login = true
}
}
```

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

### Required

- `enabled_connections` (Block Set, Min: 1) Connections that are enabled for the organization. (see [below for nested schema](#nestedblock--enabled_connections))
- `organization_id` (String) ID of the organization on which to enable the connections.

### Read-Only

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

<a id="nestedblock--enabled_connections"></a>
### Nested Schema for `enabled_connections`

Required:

- `connection_id` (String) The ID of the connection to enable for the organization.

Optional:

- `assign_membership_on_login` (Boolean) When true, all users that log in with this connection will be automatically granted membership in the organization. When false, users must be granted membership in the organization before logging in with this connection.

## Import

Import is supported using the following syntax:

```shell
# This resource can be imported by specifying the organization ID.
#
# Example:
terraform import auth0_organization_connections.my_org_conns org_XXXXX
```
4 changes: 4 additions & 0 deletions examples/resources/auth0_organization_connections/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_connections.my_org_conns org_XXXXX
28 changes: 28 additions & 0 deletions examples/resources/auth0_organization_connections/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
resource "auth0_connection" "my_connection-1" {
name = "My Connection 1"
strategy = "auth0"
}

resource "auth0_connection" "my_connection-2" {
name = "My Connection 2"
strategy = "auth0"
}

resource "auth0_organization" "my_organization" {
name = "my-organization"
display_name = "My Organization"
}

resource "auth0_organization_connections" "one-to-many" {
sergiught marked this conversation as resolved.
Show resolved Hide resolved
organization_id = auth0_organization.my_organization.id

enabled_connections {
connection_id = auth0_connection.my_connection-1.id
assign_membership_on_login = true
}

enabled_connections {
connection_id = auth0_connection.my_connection-2.id
assign_membership_on_login = true
}
}
234 changes: 234 additions & 0 deletions internal/auth0/organization/resource_connections.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package organization

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

"github.com/auth0/go-auth0"
"github.com/auth0/go-auth0/management"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-cty/cty"
"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"
)

// NewConnectionsResource will return a new auth0_organization_connections (1:many) resource.
func NewConnectionsResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"organization_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "ID of the organization on which to enable the connections.",
},
"enabled_connections": {
Type: schema.TypeSet,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"connection_id": {
Type: schema.TypeString,
Required: true,
Description: "The ID of the connection to enable for the organization.",
},
"assign_membership_on_login": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "When true, all users that log in with this connection will be " +
"automatically granted membership in the organization. When false, users must be " +
"granted membership in the organization before logging in with this connection.",
},
},
},
Required: true,
Description: "Connections that are enabled for the organization.",
},
},
CreateContext: createOrganizationConnections,
ReadContext: readOrganizationConnections,
UpdateContext: updateOrganizationConnections,
DeleteContext: deleteOrganizationConnections,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Description: "With this resource, you can manage enabled connections on an organization.",
}
}

func createOrganizationConnections(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)

alreadyEnabledConnections, err := api.Organization.Connections(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)

connectionsToAdd := expandOrganizationConnections(data.GetRawConfig().GetAttr("enabled_connections"))

if diagnostics := guardAgainstErasingUnwantedConnections(
organizationID,
alreadyEnabledConnections.OrganizationConnections,
connectionsToAdd,
); diagnostics.HasError() {
data.SetId("")
return diagnostics
}
sergiught marked this conversation as resolved.
Show resolved Hide resolved

var result *multierror.Error
for _, connection := range connectionsToAdd {
if err := api.Organization.AddConnection(organizationID, connection); err != nil {
sergiught marked this conversation as resolved.
Show resolved Hide resolved
result = multierror.Append(result, err)
}
}

if result.ErrorOrNil() != nil {
return diag.FromErr(result.ErrorOrNil())
}

return readOrganizationConnections(ctx, data, meta)
}

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

connections, err := api.Organization.Connections(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("enabled_connections", flattenOrganizationConnections(connections.OrganizationConnections)),
)

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

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

orgID := data.Id()
mutex.Lock(orgID)
defer mutex.Unlock(orgID)

var result *multierror.Error
toAdd, toRemove := value.Difference(data, "enabled_connections")

for _, rmConnection := range toRemove {
connection := rmConnection.(map[string]interface{})

if err := api.Organization.DeleteConnection(orgID, connection["connection_id"].(string)); err != nil {
if mErr, ok := err.(management.Error); ok && mErr.Status() == http.StatusNotFound {
data.SetId("")
return nil
}

result = multierror.Append(result, err)
}
}

for _, addConnection := range toAdd {
connection := addConnection.(map[string]interface{})

if err := api.Organization.AddConnection(orgID, &management.OrganizationConnection{
ConnectionID: auth0.String(connection["connection_id"].(string)),
AssignMembershipOnLogin: auth0.Bool(connection["assign_membership_on_login"].(bool)),
}); err != nil {
if mErr, ok := err.(management.Error); ok && mErr.Status() == http.StatusNotFound {
data.SetId("")
return nil
}

result = multierror.Append(result, err)
}
}

if result.ErrorOrNil() != nil {
return diag.FromErr(result.ErrorOrNil())
}

return readOrganizationConnections(ctx, data, meta)
}

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

var result *multierror.Error

connections := expandOrganizationConnections(data.GetRawState().GetAttr("enabled_connections"))
for _, conn := range connections {
err := api.Organization.DeleteConnection(data.Id(), conn.GetConnectionID())
result = multierror.Append(result, err)
}

if result.ErrorOrNil() != nil {
return diag.FromErr(result.ErrorOrNil())
}

data.SetId("")
return nil
}

func guardAgainstErasingUnwantedConnections(
organizationID string,
alreadyEnabledConnections []*management.OrganizationConnection,
connectionsToAdd []*management.OrganizationConnection,
) diag.Diagnostics {
if len(alreadyEnabledConnections) == 0 {
return nil
}

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

func expandOrganizationConnections(cfg cty.Value) []*management.OrganizationConnection {
connections := make([]*management.OrganizationConnection, 0)

cfg.ForEachElement(func(_ cty.Value, connectionCfg cty.Value) (stop bool) {
connections = append(connections, &management.OrganizationConnection{
ConnectionID: value.String(connectionCfg.GetAttr("connection_id")),
AssignMembershipOnLogin: value.Bool(connectionCfg.GetAttr("assign_membership_on_login")),
})

return stop
})

return connections
}
Loading