diff --git a/docs/resources/saml_group_mapping.md b/docs/resources/saml_group_mapping.md new file mode 100644 index 0000000..f8e1555 --- /dev/null +++ b/docs/resources/saml_group_mapping.md @@ -0,0 +1,124 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "wiz_saml_group_mapping Resource - terraform-provider-wiz" +subcategory: "" +description: |- + Configure SAML Group Role Mapping. When using SSO to authenticate with Wiz, you can map group memberships in SAML assertions to Wiz roles across specific scopes. +--- + +# wiz_saml_group_mapping (Resource) + +Configure SAML Group Role Mapping. When using SSO to authenticate with Wiz, you can map group memberships in SAML assertions to Wiz roles across specific scopes. + +## Example Usage + +```terraform +# Configure SAML Group Role Mapping on a global scope +resource "wiz_saml_group_mapping" "test_global_scope" { + saml_idp_id = "test-saml-identity-provider" + group_mappings = [ + { + provider_group_id = "global-reader-group-id" + role = "PROJECT_READER" + } + ] +} + +# Configure SAML Group Role Mapping for a single project +resource "wiz_saml_group_mapping" "test_single_project" { + saml_idp_id = "test-saml-identity-provider" + group_mappings = [ + { + provider_group_id = "admin-group-id" + role = "PROJECT_ADMIN" + projects = [ + "ee25cc95-82b0-4543-8934-5bc655b86786" + ] + } + ] +} + +# Configure SAML Group Role Mapping for multiple projects +resource "wiz_saml_group_mapping" "test_multi_project" { + saml_idp_id = "test-saml-identity-provider" + group_mappings = [ + { + provider_group_id = "member-group-id" + role = "PROJECT_MEMBER" + projects = [ + "ee25cc95-82b0-4543-8934-5bc655b86786", + "e7f6542c-81f6-43cf-af48-bdd77f09650d" + ] + } + ] +} + +# Configure multiple SAML Group Role Mappings +resource "wiz_saml_group_mapping" "test_multi_mappings" { + saml_idp_id = "test-saml-identity-provider" + group_mappings = [ + { + provider_group_id = "global-reader-group-id" + role = "PROJECT_READER" + }, + { + provider_group_id = "admin-group-id" + role = "PROJECT_ADMIN" + projects = [ + "ee25cc95-82b0-4543-8934-5bc655b86786" + ] + }, + { + provider_group_id = "member-group-id" + role = "PROJECT_MEMBER" + projects = [ + "ee25cc95-82b0-4543-8934-5bc655b86786", + "e7f6542c-81f6-43cf-af48-bdd77f09650d" + ] + } + ] +} +``` + + +## Schema + +### Required + +- `group_mapping` (Block Set, Min: 1) (see [below for nested schema](#nestedblock--group_mapping)) +- `saml_idp_id` (String) Identifier for the Saml Provider + +### Read-Only + +- `id` (String) Unique tf-internal identifier for the saml group mapping + + +### Nested Schema for `group_mapping` + +Required: + +- `provider_group_id` (String) Provider group ID +- `role` (String) Wiz Role name + +Optional: + +- `projects` (List of String) Project mapping + +## Import + +Import is supported using the following syntax: + +```shell +# The id for importing resources has to be in this format: 'mapping||::#...'. +# Import with saml mapping to multiple projects +terraform import wiz_saml_group_mapping.example_import "mapping|wiz-azure-ad-saml|88990357-fe36-421b-aedc-fcdd602b91d7:bb62aac7-e8bd-5d5e-b205-2dbafe106e1a,ee25cc95-82b0-4543-8934-5bc655b86786:PROJECT_READER" + +# Import with mapping to single project +terraform import wiz_saml_group_mapping.example_import "mapping|wiz-azure-ad-saml|88990357-fe36-421b-aedc-fcdd602b91d7:bb62aac7-e8bd-5d5e-b205-2dbafe106e1a:PROJECT_READER" + +# Import with global mapping +terraform import wiz_saml_group_mapping.example_import "mapping|wiz-azure-ad-saml|88990357-fe36-421b-aedc-fcdd602b91d7::PROJECT_READER" + +# Import with multiple group mappings +terraform import wiz_saml_group_mapping.example_import "mapping|wiz-azure-ad-saml|88990357-fe36-421b-aedc-fcdd602b91d7:bb62aac7-e8bd-5d5e-b205-2dbafe106e1a:PROJECT_READER#12345678-1234-1234-1234-123456789012:ee25cc95-82b0-4543-8934-5bc655b86786:PROJECT_WRITER" +``` diff --git a/examples/resources/wiz_saml_group_mapping/import.sh b/examples/resources/wiz_saml_group_mapping/import.sh new file mode 100644 index 0000000..0652095 --- /dev/null +++ b/examples/resources/wiz_saml_group_mapping/import.sh @@ -0,0 +1,12 @@ +# The id for importing resources has to be in this format: 'mapping||::#...'. +# Import with saml mapping to multiple projects +terraform import wiz_saml_group_mapping.example_import "mapping|wiz-azure-ad-saml|88990357-fe36-421b-aedc-fcdd602b91d7:bb62aac7-e8bd-5d5e-b205-2dbafe106e1a,ee25cc95-82b0-4543-8934-5bc655b86786:PROJECT_READER" + +# Import with mapping to single project +terraform import wiz_saml_group_mapping.example_import "mapping|wiz-azure-ad-saml|88990357-fe36-421b-aedc-fcdd602b91d7:bb62aac7-e8bd-5d5e-b205-2dbafe106e1a:PROJECT_READER" + +# Import with global mapping +terraform import wiz_saml_group_mapping.example_import "mapping|wiz-azure-ad-saml|88990357-fe36-421b-aedc-fcdd602b91d7::PROJECT_READER" + +# Import with multiple group mappings +terraform import wiz_saml_group_mapping.example_import "mapping|wiz-azure-ad-saml|88990357-fe36-421b-aedc-fcdd602b91d7:bb62aac7-e8bd-5d5e-b205-2dbafe106e1a:PROJECT_READER#12345678-1234-1234-1234-123456789012:ee25cc95-82b0-4543-8934-5bc655b86786:PROJECT_WRITER" \ No newline at end of file diff --git a/examples/resources/wiz_saml_group_mapping/resource.tf b/examples/resources/wiz_saml_group_mapping/resource.tf new file mode 100644 index 0000000..b0da5d4 --- /dev/null +++ b/examples/resources/wiz_saml_group_mapping/resource.tf @@ -0,0 +1,65 @@ +# Configure SAML Group Role Mapping on a global scope +resource "wiz_saml_group_mapping" "test_global_scope" { + saml_idp_id = "test-saml-identity-provider" + group_mappings = [ + { + provider_group_id = "global-reader-group-id" + role = "PROJECT_READER" + } + ] +} + +# Configure SAML Group Role Mapping for a single project +resource "wiz_saml_group_mapping" "test_single_project" { + saml_idp_id = "test-saml-identity-provider" + group_mappings = [ + { + provider_group_id = "admin-group-id" + role = "PROJECT_ADMIN" + projects = [ + "ee25cc95-82b0-4543-8934-5bc655b86786" + ] + } + ] +} + +# Configure SAML Group Role Mapping for multiple projects +resource "wiz_saml_group_mapping" "test_multi_project" { + saml_idp_id = "test-saml-identity-provider" + group_mappings = [ + { + provider_group_id = "member-group-id" + role = "PROJECT_MEMBER" + projects = [ + "ee25cc95-82b0-4543-8934-5bc655b86786", + "e7f6542c-81f6-43cf-af48-bdd77f09650d" + ] + } + ] +} + +# Configure multiple SAML Group Role Mappings +resource "wiz_saml_group_mapping" "test_multi_mappings" { + saml_idp_id = "test-saml-identity-provider" + group_mappings = [ + { + provider_group_id = "global-reader-group-id" + role = "PROJECT_READER" + }, + { + provider_group_id = "admin-group-id" + role = "PROJECT_ADMIN" + projects = [ + "ee25cc95-82b0-4543-8934-5bc655b86786" + ] + }, + { + provider_group_id = "member-group-id" + role = "PROJECT_MEMBER" + projects = [ + "ee25cc95-82b0-4543-8934-5bc655b86786", + "e7f6542c-81f6-43cf-af48-bdd77f09650d" + ] + } + ] +} \ No newline at end of file diff --git a/internal/acceptance/common.go b/internal/acceptance/common.go index 102d4e3..457cbb1 100644 --- a/internal/acceptance/common.go +++ b/internal/acceptance/common.go @@ -27,4 +27,6 @@ const ( TcCloudConfigRule TestCase = "CLOUD_CONFIG_RULE" // TcProjectCloudAccountLink test case TcProjectCloudAccountLink = "PROJECT_CLOUD_ACCOUNT_LINK" + // TcSAMLGroupMapping test case + TcSAMLGroupMapping TestCase = "SAML_GROUP_MAPPING" ) diff --git a/internal/acceptance/provider_test.go b/internal/acceptance/provider_test.go index 6d1f965..1e91644 100644 --- a/internal/acceptance/provider_test.go +++ b/internal/acceptance/provider_test.go @@ -49,6 +49,8 @@ func testAccPreCheck(t *testing.T, tc TestCase) { envVars = append(commonEnvVars, "WIZ_PROJECT_ID") case TcProjectCloudAccountLink: envVars = append(commonEnvVars, "WIZ_PROJECT_ID", "WIZ_SUBSCRIPTION_ID") + case TcSAMLGroupMapping: + envVars = append(commonEnvVars, "WIZ_PROJECT_ID", "WIZ_PROVIDER_GROUP_ID", "WIZ_SAML_IDP_ID") default: t.Fatalf("unknown testCase: %s", tc) } diff --git a/internal/acceptance/resource_saml_group_mapping_test.go b/internal/acceptance/resource_saml_group_mapping_test.go new file mode 100644 index 0000000..4e59fcb --- /dev/null +++ b/internal/acceptance/resource_saml_group_mapping_test.go @@ -0,0 +1,56 @@ +package acceptance + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccResourceWizSAMLGroupMapping_basic(t *testing.T) { + samlIdpID := os.Getenv("WIZ_SAML_IDP_ID") + providerGroupID := os.Getenv("WIZ_PROVIDER_GROUP_ID") + projectID := os.Getenv("WIZ_PROJECT_ID") + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t, TcSAMLGroupMapping) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testResourceWizSAMLGroupMappingBasic(samlIdpID, providerGroupID, projectID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "wiz_saml_group_mapping.foo", + "saml_idp_id", + samlIdpID, + ), + resource.TestCheckResourceAttr( + "wiz_saml_group_mapping.foo", + "group_mapping.0.provider_group_id", + providerGroupID, + ), + resource.TestCheckResourceAttr( + "wiz_saml_group_mapping.foo", + "group_mapping.0.projects.0", + projectID, + ), + ), + }, + }, + }) +} + +func testResourceWizSAMLGroupMappingBasic(samlIdpID string, providerGroupID string, projectID string) string { + return fmt.Sprintf(` + resource "wiz_saml_group_mapping" "foo" { + saml_idp_id = "%s" + group_mapping { + provider_group_id = "%s" + role = "PROJECT_READER" + projects = [ + "%s" + ] + } + }`, samlIdpID, providerGroupID, projectID) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 8d89428..15cc640 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -299,6 +299,7 @@ yLyKQXhw2W2Xs0qLeC1etA+jTGDK4UfLeC0SF7FSi8o5LL21L8IzApar2pR/ "wiz_report_graph_query": resourceWizReportGraphQuery(), "wiz_project": resourceWizProject(), "wiz_saml_idp": resourceWizSAMLIdP(), + "wiz_saml_group_mapping": resourceWizSAMLGroupMapping(), "wiz_security_framework": resourceWizSecurityFramework(), "wiz_service_account": resourceWizServiceAccount(), "wiz_user": resourceWizUser(), diff --git a/internal/provider/resource_saml_group_mapping.go b/internal/provider/resource_saml_group_mapping.go new file mode 100644 index 0000000..e14f265 --- /dev/null +++ b/internal/provider/resource_saml_group_mapping.go @@ -0,0 +1,438 @@ +package provider + +import ( + "context" + "errors" + "slices" + "strings" + + "github.com/google/uuid" + "wiz.io/hashicorp/terraform-provider-wiz/internal/utils" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "wiz.io/hashicorp/terraform-provider-wiz/internal" + "wiz.io/hashicorp/terraform-provider-wiz/internal/client" + "wiz.io/hashicorp/terraform-provider-wiz/internal/wiz" +) + +// ReadSAMLGroupMappings represents the structure of a SAML group mappings read operation. +// It includes a SAMLIdentityProviderGroupMappingsConnection object. +type ReadSAMLGroupMappings struct { + SAMLGroupMappings wiz.SAMLIdentityProviderGroupMappingsConnection `json:"samlIdentityProviderGroupMappings"` +} + +// UpdateSAMLGroupMappingInput struct +type UpdateSAMLGroupMappingInput struct { + ID string `json:"id"` + Patch wiz.ModifySAMLGroupMappingPatch `json:"patch"` +} + +// SAMLGroupMappingsImport struct +type SAMLGroupMappingsImport struct { + SamlIdpID string + GroupMappings []wiz.SAMLGroupDetailsInput +} + +// UpdateSAMLGroupMappingPayload struct +type UpdateSAMLGroupMappingPayload struct { + SAMLGroupMapping wiz.SAMLGroupMapping `json:"samlGroupMapping,omitempty"` +} + +func resourceWizSAMLGroupMapping() *schema.Resource { + return &schema.Resource{ + Description: "Configure SAML Group Role Mapping. When using SSO to authenticate with Wiz, you can map group memberships in SAML assertions to Wiz roles across specific scopes.", + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "Unique tf-internal identifier for the saml group mapping", + Computed: true, + }, + "saml_idp_id": { + Type: schema.TypeString, + Description: "Identifier for the Saml Provider", + Required: true, + ForceNew: true, + }, + "group_mapping": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "provider_group_id": { + Type: schema.TypeString, + Description: "Provider group ID", + Required: true, + }, + "role": { + Type: schema.TypeString, + Description: "Wiz Role name", + Required: true, + }, + "projects": { + Type: schema.TypeList, + Optional: true, + Description: "Project mapping", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + }, + CreateContext: resourceSAMLGroupMappingCreate, + ReadContext: resourceSAMLGroupMappingRead, + UpdateContext: resourceSAMLGroupMappingUpdate, + DeleteContext: resourceSAMLGroupMappingDelete, + Importer: &schema.ResourceImporter{ + + StateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + // schema for import id: | + mappingToImport, err := extractIDsFromSamlIdpGroupMappingImportID(d.Id()) + if err != nil { + return nil, err + } + + err = d.Set("saml_idp_id", mappingToImport.SamlIdpID) + if err != nil { + return nil, err + } + + var groupMappings []map[string]interface{} + for _, groupMapping := range mappingToImport.GroupMappings { + groupMappingMap := map[string]interface{}{ + "provider_group_id": groupMapping.ProviderGroupID, + "role": groupMapping.Role, + "projects": groupMapping.Projects, + } + groupMappings = append(groupMappings, groupMappingMap) + } + + err = d.Set("group_mappings", groupMappings) + if err != nil { + return nil, err + } + + d.SetId(uuid.NewString()) + return []*schema.ResourceData{d}, nil + }, + }, + } +} + +func resourceSAMLGroupMappingCreate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizSAMLGroupMappingCreate called...") + samlIdpID := d.Get("saml_idp_id").(string) + groupMappings := d.Get("group_mapping").(*schema.Set).List() + + var upsertGroupMappings []wiz.SAMLGroupDetailsInput + for _, item := range groupMappings { + groupMapping := item.(map[string]interface{}) + providerGroupID := groupMapping["provider_group_id"].(string) + role := groupMapping["role"].(string) + projectIDs := utils.ConvertListToString(groupMapping["projects"].([]interface{})) + + // verify the mapping doesn't already exist + matchingNodes, diags := querySAMLGroupMappings(ctx, m, samlIdpID, groupMappings) + if len(diags) != 0 { + return diags + } + + for _, matchingNode := range matchingNodes { + if matchingNode.ProviderGroupID == providerGroupID && matchingNode.Role.ID == role && slices.Equal(projectIDs, extractProjectIDs(matchingNode.Projects)) { + return diag.Errorf("saml group mapping for group: %s and role: %s to project(s): %s already exists for saml idp provider: %s and should be imported instead", + providerGroupID, role, strings.Join(projectIDs, ", "), samlIdpID) + } + } + + upsertGroupMapping := wiz.SAMLGroupDetailsInput{ + ProviderGroupID: providerGroupID, + Role: role, + Projects: projectIDs, + } + upsertGroupMappings = append(upsertGroupMappings, upsertGroupMapping) + } + + // define the graphql query + query := `mutation SetSAMLGroupMapping ($input: ModifySAMLGroupMappingInput!) { + modifySAMLIdentityProviderGroupMappings(input: $input) { + _stub + } + }` + + // populate the graphql variables + vars := &UpdateSAMLGroupMappingInput{} + vars.ID = samlIdpID + vars.Patch = wiz.ModifySAMLGroupMappingPatch{ + Upsert: &upsertGroupMappings, + } + + // process the request + data := &UpdateSAMLGroupMappingPayload{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "saml_group_mapping", "create") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + // set the id + d.SetId(uuid.NewString()) + + return resourceSAMLGroupMappingRead(ctx, d, m) +} + +func extractIDsFromSamlIdpGroupMappingImportID(id string) (SAMLGroupMappingsImport, error) { + parts := strings.Split(id, "|") + + if len(parts) != 3 { + return SAMLGroupMappingsImport{}, errors.New("invalid ID format") + } + + groupMappingStrings := strings.Split(parts[2], "#") + var groupMappings []wiz.SAMLGroupDetailsInput + for _, groupMappingString := range groupMappingStrings { + groupMappingParts := strings.Split(groupMappingString, ":") + if len(groupMappingParts) < 2 { + return SAMLGroupMappingsImport{}, errors.New("invalid group mapping format") + } + + providerGroupID := groupMappingParts[0] + role := groupMappingParts[1] + var projectIDs []string + if len(groupMappingParts) > 2 && groupMappingParts[2] != "" { + projectIDs = strings.Split(groupMappingParts[2], ",") + } + + groupMapping := wiz.SAMLGroupDetailsInput{ + ProviderGroupID: providerGroupID, + Role: role, + Projects: projectIDs, + } + groupMappings = append(groupMappings, groupMapping) + + } + + return SAMLGroupMappingsImport{ + SamlIdpID: parts[1], + GroupMappings: groupMappings, + }, nil +} + +func extractProjectIDs(projects []wiz.Project) []string { + projectIDs := make([]string, len(projects)) + for i, project := range projects { + projectIDs[i] = project.ID + } + + return projectIDs +} + +func resourceSAMLGroupMappingRead(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizSAMLGroupMappingRead called...") + + // check the id + if d.Id() == "" { + return nil + } + + samlIdpID := d.Get("saml_idp_id").(string) + groupMappings := d.Get("group_mapping").(*schema.Set).List() + + var newGroupMappings []interface{} + + matchingNodes, diags := querySAMLGroupMappings(ctx, m, samlIdpID, groupMappings) + if len(diags) > 0 { + return diags + } + + for _, item := range groupMappings { + groupMapping := item.(map[string]interface{}) + providerGroupID := groupMapping["provider_group_id"].(string) + role := groupMapping["role"].(string) + projectIDs := utils.ConvertListToString(groupMapping["projects"].([]interface{})) + + for _, matchingNode := range matchingNodes { + if matchingNode.ProviderGroupID == providerGroupID && matchingNode.Role.ID == role && slices.Equal(projectIDs, extractProjectIDs(matchingNode.Projects)) { + // set the resource parameters + newGroupMapping := map[string]interface{}{ + "provider_group_id": matchingNode.ProviderGroupID, + "role": matchingNode.Role.ID, + "projects": extractProjectIDs(matchingNode.Projects), + } + newGroupMappings = append(newGroupMappings, newGroupMapping) + } + } + } + + err := d.Set("saml_idp_id", samlIdpID) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + err = d.Set("group_mapping", newGroupMappings) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + return diags + +} + +func resourceSAMLGroupMappingUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizSAMLGroupMappingUpdate called...") + + // check the id + if d.Id() == "" { + return nil + } + + samlIdpID := d.Get("saml_idp_id").(string) + groupMappings := d.Get("group_mapping").(*schema.Set).List() + var upsertGroupMappings []wiz.SAMLGroupDetailsInput + for _, item := range groupMappings { + groupMapping := item.(map[string]interface{}) + providerGroupID := groupMapping["provider_group_id"].(string) + role := groupMapping["role"].(string) + projects := utils.ConvertListToString(groupMapping["projects"].([]interface{})) + upsertGroupMapping := wiz.SAMLGroupDetailsInput{ + ProviderGroupID: providerGroupID, + Role: role, + Projects: projects, + } + upsertGroupMappings = append(upsertGroupMappings, upsertGroupMapping) + } + + // define the graphql query + query := `mutation SetSAMLGroupMapping ($input: ModifySAMLGroupMappingInput!) { + modifySAMLIdentityProviderGroupMappings(input: $input) { + _stub + } + }` + + // populate the graphql variables + vars := &UpdateSAMLGroupMappingInput{} + vars.ID = samlIdpID + vars.Patch = wiz.ModifySAMLGroupMappingPatch{ + Upsert: &upsertGroupMappings, + } + + // process the request + data := &UpdateSAMLGroupMappingPayload{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "saml_group_mapping", "update") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + return resourceSAMLGroupMappingRead(ctx, d, m) +} + +func resourceSAMLGroupMappingDelete(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizSAMLGroupMappingDelete called...") + + // check the id + if d.Id() == "" { + return nil + } + + samlIdpID := d.Get("saml_idp_id").(string) + groupMappings := d.Get("group_mapping").(*schema.Set).List() + + var deleteGroupMappings []string + for _, item := range groupMappings { + groupMapping := item.(map[string]interface{}) + providerGroupID := groupMapping["provider_group_id"].(string) + deleteGroupMappings = append(deleteGroupMappings, providerGroupID) + } + + // define the graphql query + query := `mutation SetSAMLGroupMapping ($input: ModifySAMLGroupMappingInput!) { + modifySAMLIdentityProviderGroupMappings(input: $input) { + _stub + } + }` + + // populate the graphql variables + vars := &UpdateSAMLGroupMappingInput{} + vars.ID = samlIdpID + vars.Patch = wiz.ModifySAMLGroupMappingPatch{ + Delete: &deleteGroupMappings, + } + + // process the request + data := &UpdateSAMLGroupMappingPayload{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "saml_group_mapping", "delete") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + return diags +} + +func querySAMLGroupMappings(ctx context.Context, m interface{}, samlIdpID string, groupMappings []interface{}) ([]*wiz.SAMLGroupMapping, diag.Diagnostics) { + // define the graphql query + query := `query samlIdentityProviderGroupMappings ($id: ID!, $first: Int! $after: String){ + samlIdentityProviderGroupMappings ( + id: $id, + first: $first + after: $after + ) { + pageInfo { + hasNextPage + endCursor + } + nodes { + providerGroupId + role { + id + } + projects { + id + } + } + } + }` + + // populate the graphql variables + vars := &internal.QueryVariables{} + vars.ID = samlIdpID + vars.First = 500 + + // Call ProcessPagedRequest + diags, allData := client.ProcessPagedRequest(ctx, m, vars, &ReadSAMLGroupMappings{}, query, "saml_idp", "read", 0) + if diags.HasError() { + return nil, diags + } + + var matchingNodes []*wiz.SAMLGroupMapping + + // Process the data... + for _, data := range allData { + typedData, ok := data.(*ReadSAMLGroupMappings) + if !ok { + return nil, diag.Errorf("data is not of type *ReadSAMLGroupMappings") + } + + nodes := typedData.SAMLGroupMappings.Nodes + for _, node := range nodes { + for _, item := range groupMappings { + groupMapping := item.(map[string]interface{}) + providerGroupID := groupMapping["provider_group_id"].(string) + roleID := groupMapping["role"].(string) + projectIDs := utils.ConvertListToString(groupMapping["projects"].([]interface{})) + nodeProjectIDs := extractProjectIDs(node.Projects) + + // If we find a match, store the node + if node.ProviderGroupID == providerGroupID && node.Role.ID == roleID && slices.Equal(projectIDs, nodeProjectIDs) { + matchingNodes = append(matchingNodes, node) + } + } + } + } + + return matchingNodes, nil +} diff --git a/internal/provider/resource_saml_group_mapping_test.go b/internal/provider/resource_saml_group_mapping_test.go new file mode 100644 index 0000000..c05d799 --- /dev/null +++ b/internal/provider/resource_saml_group_mapping_test.go @@ -0,0 +1,55 @@ +package provider + +import ( + "reflect" + + "wiz.io/hashicorp/terraform-provider-wiz/internal/wiz" + + "testing" +) + +func TestExtractIDsFromSamlIdpGroupMappingImportID(t *testing.T) { + testCases := []struct { + name string + input string + expectedMapping SAMLGroupMappingsImport + expectErr bool + }{ + { + name: "Valid ID", + input: "link|samlIdpID|providerGroupID:role:projectID1,projectID2", + expectedMapping: SAMLGroupMappingsImport{SamlIdpID: "samlIdpID", GroupMappings: []wiz.SAMLGroupDetailsInput{{ProviderGroupID: "providerGroupID", Role: "role", Projects: []string{"projectID1", "projectID2"}}}}, + expectErr: false, + }, + { + name: "Valid ID global mapping", + input: "link|samlIdpID|providerGroupID:role", + expectedMapping: SAMLGroupMappingsImport{SamlIdpID: "samlIdpID", GroupMappings: []wiz.SAMLGroupDetailsInput{{ProviderGroupID: "providerGroupID", Role: "role", Projects: nil}}}, + expectErr: false, + }, + { + name: "Invalid ID", + input: "invalidId", + expectedMapping: SAMLGroupMappingsImport{}, + expectErr: true, + }, + { + name: "Invalid ID length", + input: "link|samlIdpId", + expectedMapping: SAMLGroupMappingsImport{}, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mapping, err := extractIDsFromSamlIdpGroupMappingImportID(tc.input) + if (err != nil) != tc.expectErr { + t.Errorf("Expected error: %v, got: %v", tc.expectErr, err) + } + if !reflect.DeepEqual(mapping, tc.expectedMapping) { + t.Errorf("Expected mapping: %+v, got: %+v", tc.expectedMapping, mapping) + } + }) + } +} diff --git a/internal/wiz/structs.go b/internal/wiz/structs.go index f0362f4..8195831 100644 --- a/internal/wiz/structs.go +++ b/internal/wiz/structs.go @@ -244,6 +244,26 @@ type SAMLGroupMappingCreateInput struct { Projects []string `json:"projects"` } +// SAMLGroupDetailsInput struct +// Incomplete because 'description' field is missing as in the schema +type SAMLGroupDetailsInput struct { + ProviderGroupID string `json:"providerGroupId"` + Role string `json:"role"` + Projects []string `json:"projects"` +} + +// ModifySAMLGroupMappingPatch struct +type ModifySAMLGroupMappingPatch struct { + Upsert *[]SAMLGroupDetailsInput `json:"upsert,omitempty"` + Delete *[]string `json:"delete,omitempty"` +} + +// SAMLIdentityProviderGroupMappingsConnection struct +type SAMLIdentityProviderGroupMappingsConnection struct { + PageInfo PageInfo `json:"pageInfo"` + Nodes []*SAMLGroupMapping `json:"nodes,omitempty"` +} + // SAMLIdentityProvider struct -- updates type SAMLIdentityProvider struct { AllowManualRoleOverride *bool `json:"allowManualRoleOverride"`