Skip to content

Commit

Permalink
Refactor resourceUserGroupAssociation to support import
Browse files Browse the repository at this point in the history
  • Loading branch information
cheelim1 committed Nov 2, 2024
1 parent a735daf commit 452f63b
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 26 deletions.
71 changes: 56 additions & 15 deletions jumpcloud/resource_user_group_association.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package jumpcloud
import (
"context"
"fmt"
"strings"

jcapiv2 "github.com/TheJumpCloud/jcapi-go/v2"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
Expand All @@ -11,11 +12,14 @@ import (

func resourceUserGroupAssociation() *schema.Resource {
return &schema.Resource{
Description: "Provides a resource for associating a JumpCloud user group to objects like SSO applications, G Suite, Office 365, LDAP and more.",
Description: "Provides a resource for associating a JumpCloud user group to objects like SSO applications, G Suite, Office 365, LDAP, and more.",
Create: resourceUserGroupAssociationCreate,
Read: resourceUserGroupAssociationRead,
Update: nil,
Delete: resourceUserGroupAssociationDelete,
Importer: &schema.ResourceImporter{
State: resourceUserGroupAssociationImport,
},
Schema: map[string]*schema.Schema{
"group_id": {
Description: "The ID of the `resource_user_group` resource.",
Expand All @@ -24,13 +28,13 @@ func resourceUserGroupAssociation() *schema.Resource {
ForceNew: true,
},
"object_id": {
Description: "The ID of the object to associate to the group.",
Description: "The ID of the object to associate with the group.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"type": {
Description: "The type of the object to associate to the given group. Possible values: `active_directory`, `application`, `command`, `g_suite`, `ldap_server`, `office_365`, `policy`, `radius_server`, `system`, `system_group`.",
Description: "The type of the object to associate with the given group. Possible values: `active_directory`, `application`, `command`, `g_suite`, `ldap_server`, `office_365`, `policy`, `radius_server`, `system`, `system_group`.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
Expand Down Expand Up @@ -59,9 +63,7 @@ func resourceUserGroupAssociation() *schema.Resource {
}
}

func modifyUserGroupAssociation(client *jcapiv2.APIClient,
d *schema.ResourceData, action string) diag.Diagnostics {

func modifyUserGroupAssociation(client *jcapiv2.APIClient, d *schema.ResourceData, action string) diag.Diagnostics {
payload := jcapiv2.UserGroupGraphManagementReq{
Op: action,
Type_: d.Get("type").(string),
Expand All @@ -86,44 +88,83 @@ func resourceUserGroupAssociationCreate(d *schema.ResourceData, meta interface{}
if diags.HasError() {
return fmt.Errorf("Error creating user group association: %v", diags)
}

// Set the resource ID
d.SetId(fmt.Sprintf("%s/%s/%s", d.Get("group_id").(string), d.Get("object_id").(string), d.Get("type").(string)))

return resourceUserGroupAssociationRead(d, meta)
}

func resourceUserGroupAssociationRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*jcapiv2.Configuration)
client := jcapiv2.NewAPIClient(config)

// Retrieve the group_id, object_id, and type from the resource data
groupID := d.Get("group_id").(string)
objectID := d.Get("object_id").(string)
objectType := d.Get("type").(string)

// Prepare optional parameters for the API call
optionals := map[string]interface{}{
"groupId": d.Get("group_id").(string),
"groupId": groupID,
"limit": int32(100),
}

graphconnect, _, err := client.UserGroupAssociationsApi.GraphUserGroupAssociationsList(
context.TODO(), d.Get("group_id").(string), "", "", []string{d.Get("type").(string)}, optionals)
// Fetch associations for the group
graphConnect, _, err := client.UserGroupAssociationsApi.GraphUserGroupAssociationsList(
context.TODO(), groupID, "", "", []string{objectType}, optionals)
if err != nil {
return err
}

// the ID of the specified object is buried in a complex construct
for _, v := range graphconnect {
if v.To.Id == d.Get("object_id") {
resourceId := d.Get("group_id").(string) + "/" + d.Get("object_id").(string)
d.SetId(resourceId)
// Check if the specified association exists
for _, v := range graphConnect {
if v.To.Id == objectID {
// Resource exists
d.SetId(fmt.Sprintf("%s/%s/%s", groupID, objectID, objectType))
return nil
}
}

// element does not exist; unset ID
// If the association does not exist, unset ID to signal resource removal
d.SetId("")
return nil
}

func resourceUserGroupAssociationDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*jcapiv2.Configuration)
client := jcapiv2.NewAPIClient(config)

diags := modifyUserGroupAssociation(client, d, "remove")
if diags.HasError() {
return fmt.Errorf("Error deleting user group association: %v", diags)
}
return nil
}

func resourceUserGroupAssociationImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
// Expected ID format: <group_id>/<object_id>/<type>
idParts := strings.Split(d.Id(), "/")
if len(idParts) != 3 {
return nil, fmt.Errorf("unexpected format of ID (%s), expected <group_id>/<object_id>/<type>", d.Id())
}
groupID := idParts[0]
objectID := idParts[1]
objectType := idParts[2]

// Set the parsed values into the resource data
if err := d.Set("group_id", groupID); err != nil {
return nil, err
}
if err := d.Set("object_id", objectID); err != nil {
return nil, err
}
if err := d.Set("type", objectType); err != nil {
return nil, err
}

// Set the ID again to ensure consistency
d.SetId(fmt.Sprintf("%s/%s/%s", groupID, objectID, objectType))

return []*schema.ResourceData{d}, nil
}
111 changes: 100 additions & 11 deletions jumpcloud/resource_user_group_association_test.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,51 @@
package jumpcloud

import (
"encoding/json"
"fmt"
"net/http"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
)

func TestAccUserGroupAssociation(t *testing.T) {
randSuffix := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
randSuffix := acctest.RandString(10)
resourceName := "jumpcloud_user_group_association.test_association"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: nil,
CheckDestroy: testAccCheckUserGroupAssociationDestroy,
Steps: []resource.TestStep{
{
Config: testUserGroupAssocConfig(randSuffix),
Check: resource.TestCheckResourceAttrSet("jumpcloud_user_group_association.test_association",
"group_id"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(resourceName, "group_id"),
resource.TestCheckResourceAttrSet(resourceName, "object_id"),
resource.TestCheckResourceAttrSet(resourceName, "type"),
),
},
{
// Test Import Functionality
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateIdFunc: func(s *terraform.State) (string, error) {
// Retrieve the resource from the state
rs, ok := s.RootModule().Resources[resourceName]
if !ok {
return "", fmt.Errorf("Not found: %s", resourceName)
}
groupID := rs.Primary.Attributes["group_id"]
objectID := rs.Primary.Attributes["object_id"]
objectType := rs.Primary.Attributes["type"]

return fmt.Sprintf("%s/%s/%s", groupID, objectID, objectType), nil
},
},
},
})
Expand All @@ -28,18 +54,81 @@ func TestAccUserGroupAssociation(t *testing.T) {
func testUserGroupAssocConfig(randSuffix string) string {
return fmt.Sprintf(`
resource "jumpcloud_application" "test_application" {
display_name = "test_aws_account_%s"
sso_url = "https://sso.jumpcloud.com/saml2/example-application-%s"
display_name = "test_application_%s"
sso_url = "https://sso.jumpcloud.com/saml2/example-application-%s"
}
resource "jumpcloud_user_group" "test_group" {
name = "testgroup_%s"
name = "testgroup_%s"
}
resource "jumpcloud_user_group_association" "test_association" {
object_id = jumpcloud_application.test_application.id
group_id = jumpcloud_user_group.test_group.id
type = "application"
object_id = jumpcloud_application.test_application.id
group_id = jumpcloud_user_group.test_group.id
type = "application"
}
`, randSuffix, randSuffix, randSuffix)
}

// CheckDestroy function to ensure the resource is properly destroyed.
func testAccCheckUserGroupAssociationDestroy(s *terraform.State) error {
// Since we're not using the JumpCloud Go SDK v2, we'll use HTTP requests.
apiKey := os.Getenv("JUMPCLOUD_API_KEY")
if apiKey == "" {
return fmt.Errorf("JUMPCLOUD_API_KEY must be set for acceptance tests")
}

client := &http.Client{}
for _, rs := range s.RootModule().Resources {
if rs.Type != "jumpcloud_user_group_association" {
continue
}

groupID := rs.Primary.Attributes["group_id"]
objectID := rs.Primary.Attributes["object_id"]
objectType := rs.Primary.Attributes["type"]

// Build the request URL.
url := fmt.Sprintf("https://console.jumpcloud.com/api/v2/usergroups/%s/%s", groupID, objectType)

// Prepare the request.
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
req.Header.Add("x-api-key", apiKey)
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")

// Execute the request.
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

// Check for non-200 status codes.
if resp.StatusCode != 200 {
// If the resource is not found, it's successfully destroyed.
if resp.StatusCode == 404 {
continue
}
return fmt.Errorf("Failed to verify destruction of user group association: %s", resp.Status)
}

// Parse the response to check if the association still exists.
var associations []map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&associations)
if err != nil {
return err
}

for _, association := range associations {
if association["id"] == objectID {
return fmt.Errorf("User group association still exists: %s/%s/%s", groupID, objectID, objectType)
}
}
}

return nil
}

0 comments on commit 452f63b

Please sign in to comment.