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"`