From 59e362796367cb02ed499a4c2c5e6e2856a28ffc Mon Sep 17 00:00:00 2001 From: JP Schoombee Date: Tue, 12 Dec 2023 22:21:02 +0100 Subject: [PATCH] New Resource: wiz_connector_gcp (#164) * New Resource: wiz_connector_gcp * fix: struct tags --- docs/resources/connector_gcp.md | 109 ++++ .../resources/wiz_connector_gcp/import.sh | 18 + .../resources/wiz_connector_gcp/resource.tf | 39 ++ ...test.go => resource_connector_aws_test.go} | 0 .../acceptance/resource_connector_gcp_test.go | 83 +++ internal/provider/provider.go | 22 + internal/provider/resource_connector_aws.go | 44 +- internal/provider/resource_connector_gcp.go | 479 ++++++++++++++++++ .../provider/resource_connector_gcp_test.go | 47 ++ internal/wiz/structs.go | 36 ++ 10 files changed, 845 insertions(+), 32 deletions(-) create mode 100644 docs/resources/connector_gcp.md create mode 100644 examples/resources/wiz_connector_gcp/import.sh create mode 100644 examples/resources/wiz_connector_gcp/resource.tf rename internal/acceptance/{resource_connector_test.go => resource_connector_aws_test.go} (100%) create mode 100644 internal/acceptance/resource_connector_gcp_test.go create mode 100644 internal/provider/resource_connector_gcp.go create mode 100644 internal/provider/resource_connector_gcp_test.go diff --git a/docs/resources/connector_gcp.md b/docs/resources/connector_gcp.md new file mode 100644 index 0000000..64ce107 --- /dev/null +++ b/docs/resources/connector_gcp.md @@ -0,0 +1,109 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "wiz_connector_gcp Resource - terraform-provider-wiz" +subcategory: "" +description: |- + Connectors are used to connect GCP resources to Wiz. +--- + +# wiz_connector_gcp (Resource) + +Connectors are used to connect GCP resources to Wiz. + +## Example Usage + +```terraform +# Provision a simple GCP connector, organization-wide +resource "wiz_connector_gcp" "example" { + name = "example" + auth_params = jsonencode({ + "isManagedIdentity" : true, + "organization_id" : "o-example" + }) + + extra_config = jsonencode( + { + "projects" : [], + "excludedProjects" : [], + "includedFolders" : [], + "excludedFolders" : [], + "diskAnalyzerInFlightDisabled" : false, + "auditLogMonitorEnabled" : false + } + ) +} + +# Provision a GCP connector targeting an individual Google project +resource "wiz_connector_gcp" "example" { + name = "example" + auth_params = jsonencode({ + "isManagedIdentity" : true, + "project_id" : "exmaple-project-id" + }) + + extra_config = jsonencode( + { + "projects" : [], + "excludedProjects" : [], + "includedFolders" : [], + "excludedFolders" : [], + "diskAnalyzerInFlightDisabled" : false, + "auditLogMonitorEnabled" : false + } + ) +} +``` + + +## Schema + +### Required + +- `auth_params` (String, Sensitive) The authentication parameters. Must be represented in `JSON` format. +- `name` (String) The connector name. + +### Optional + +- `enabled` (Boolean) Whether the connector is enabled. + - Defaults to `true`. +- `extra_config` (String) Extra configuration for the connector. Must be represented in `JSON` format. + +### Read-Only + +- `audit_log_monitor_enabled` (Boolean) Whether audit log monitor is enabled. Note an advanced license is required. +- `disk_analyzer_inflight_disabled` (Boolean) If using Outpost, whether disk analyzer inflight scanning is disabled. +- `events_pub_sub_subscription_id` (String) If using Wiz Cloud Events, the Pub/Sub Subscription ID. +- `events_topic_name` (String) If using Wiz Cloud Events, the Topic Name in format `projects//topics/`. +- `excluded_folders` (List of String) The GCP folders excluded by the connector. +- `excluded_projects` (List of String) The GCP projects excluded by the connector. +- `folder_id` (String) The GCP folder ID. +- `id` (String) Wiz internal identifier for the connector. +- `included_folders` (List of String) The GCP folders included by the connector. +- `is_managed_identity` (String) Is managed identity? +- `organization_id` (String) The GCP organization ID. +- `projects` (List of String) The GCP projects to target with the connector. + +## Import + +Import is supported using the following syntax: + +```shell +# Importing Considerations: +# +# Please note this is considered experimental, exercise caution and consider the following: +# +# - Make sure that the `auth_params` field is set to the same values as set when the resource was created outside of Terraform. +# This is due to the way we need to handle change as under normal diff conditions, `auth_params` requires a resource recreation. +# +# - For `auth_params` include `isManagedIdentity`. If using outposts, also include `outPostId` and `diskAnalyzer` structure. +# +# For more information, refer to the examples in the documentation. +# +terraform import wiz_connector_gcp.import_example "7be792ba-bfd1-46d0-9fba-5f6bc19df4a8" + +# Optional - this is to set auth_params in state. +# +# If not run post-import, the next `terraform apply` will take care of it. +# Note any speculative changes to `auth_params` are for setting state for the one-time import only, any further changes would require a resource recreation as normal. +terraform apply --target=wiz_connector_gcp.import_example +``` diff --git a/examples/resources/wiz_connector_gcp/import.sh b/examples/resources/wiz_connector_gcp/import.sh new file mode 100644 index 0000000..5bd5059 --- /dev/null +++ b/examples/resources/wiz_connector_gcp/import.sh @@ -0,0 +1,18 @@ +# Importing Considerations: +# +# Please note this is considered experimental, exercise caution and consider the following: +# +# - Make sure that the `auth_params` field is set to the same values as set when the resource was created outside of Terraform. +# This is due to the way we need to handle change as under normal diff conditions, `auth_params` requires a resource recreation. +# +# - For `auth_params` include `isManagedIdentity`. If using outposts, also include `outPostId` and `diskAnalyzer` structure. +# +# For more information, refer to the examples in the documentation. +# +terraform import wiz_connector_gcp.import_example "7be792ba-bfd1-46d0-9fba-5f6bc19df4a8" + +# Optional - this is to set auth_params in state. +# +# If not run post-import, the next `terraform apply` will take care of it. +# Note any speculative changes to `auth_params` are for setting state for the one-time import only, any further changes would require a resource recreation as normal. +terraform apply --target=wiz_connector_gcp.import_example \ No newline at end of file diff --git a/examples/resources/wiz_connector_gcp/resource.tf b/examples/resources/wiz_connector_gcp/resource.tf new file mode 100644 index 0000000..b2a2f79 --- /dev/null +++ b/examples/resources/wiz_connector_gcp/resource.tf @@ -0,0 +1,39 @@ +# Provision a simple GCP connector, organization-wide +resource "wiz_connector_gcp" "example" { + name = "example" + auth_params = jsonencode({ + "isManagedIdentity" : true, + "organization_id" : "o-example" + }) + + extra_config = jsonencode( + { + "projects" : [], + "excludedProjects" : [], + "includedFolders" : [], + "excludedFolders" : [], + "diskAnalyzerInFlightDisabled" : false, + "auditLogMonitorEnabled" : false + } + ) +} + +# Provision a GCP connector targeting an individual Google project +resource "wiz_connector_gcp" "example" { + name = "example" + auth_params = jsonencode({ + "isManagedIdentity" : true, + "project_id" : "exmaple-project-id" + }) + + extra_config = jsonencode( + { + "projects" : [], + "excludedProjects" : [], + "includedFolders" : [], + "excludedFolders" : [], + "diskAnalyzerInFlightDisabled" : false, + "auditLogMonitorEnabled" : false + } + ) +} diff --git a/internal/acceptance/resource_connector_test.go b/internal/acceptance/resource_connector_aws_test.go similarity index 100% rename from internal/acceptance/resource_connector_test.go rename to internal/acceptance/resource_connector_aws_test.go diff --git a/internal/acceptance/resource_connector_gcp_test.go b/internal/acceptance/resource_connector_gcp_test.go new file mode 100644 index 0000000..a926a67 --- /dev/null +++ b/internal/acceptance/resource_connector_gcp_test.go @@ -0,0 +1,83 @@ +package acceptance + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccResourceWizConnectorGcp_basic(t *testing.T) { + rName := acctest.RandomWithPrefix(ResourcePrefix) + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t, TestCase(TcCommon)) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testResourceWizConnectorGcpBasic(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "wiz_connector_gcp.foo", + "name", + rName, + ), + resource.TestCheckResourceAttr( + "wiz_connector_gcp.foo", + "folder_id", + "123456", + ), + resource.TestCheckResourceAttr( + "wiz_connector_gcp.foo", + "auth_params", + "{\"folder_id\":\"123456\",\"isManagedIdentity\":true}", + ), + resource.TestMatchResourceAttr( + "wiz_connector_gcp.foo", + "id", + regexp.MustCompile(UUIDPattern), + ), + resource.TestCheckResourceAttr( + "wiz_connector_gcp.foo", + "enabled", + "true", + ), + resource.TestCheckResourceAttr( + "wiz_connector_gcp.foo", + "disk_analyzer_inflight_disabled", + "false", + ), + resource.TestCheckResourceAttr( + "wiz_connector_gcp.foo", + "extra_config", + "{\"auditLogMonitorEnabled\":false,\"diskAnalyzerInFlightDisabled\":false,\"excludedFolders\":[],\"excludedProjects\":[],\"includedFolders\":[],\"projects\":[]}", + ), + ), + }, + }, + }) +} + +func testResourceWizConnectorGcpBasic(rName string) string { + return fmt.Sprintf(` + resource "wiz_connector_gcp" "foo" { + name = "%[1]s" + auth_params = jsonencode({ + "isManagedIdentity" : true, + "folder_id" : "123456", + }) + extra_config = jsonencode( + { + "projects" : [], + "excludedProjects" : [], + "includedFolders" : [], + "excludedFolders" : [], + "diskAnalyzerInFlightDisabled" : false, + "auditLogMonitorEnabled" : false, + } + ) + } +`, rName) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 4438ce6..bfb3bc9 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -11,8 +11,29 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "wiz.io/hashicorp/terraform-provider-wiz/internal/config" + "wiz.io/hashicorp/terraform-provider-wiz/internal/wiz" ) +// CreateConnector struct +type CreateConnector struct { + CreateConnector wiz.CreateConnectorPayload `json:"createConnector"` +} + +// ReadConnectorPayload struct +type ReadConnectorPayload struct { + Connector wiz.Connector `json:"connector"` +} + +// UpdateConnector struct +type UpdateConnector struct { + UpdateConnector wiz.UpdateConnectorPayload `json:"updateConnector"` +} + +// DeleteConnector struct +type DeleteConnector struct { + DeleteConnector wiz.DeleteConnectorPayload `json:"_stub"` +} + // New creates a new provider func New(version string) func() *schema.Provider { return func() *schema.Provider { @@ -270,6 +291,7 @@ yLyKQXhw2W2Xs0qLeC1etA+jTGDK4UfLeC0SF7FSi8o5LL21L8IzApar2pR/ "wiz_control": resourceWizControl(), "wiz_control_associations": resourceWizControlAssociations(), "wiz_connector_aws": resourceWizConnectorAws(), + "wiz_connector_gcp": resourceWizConnectorGcp(), "wiz_host_config_rule_associations": resourceWizHostConfigRuleAssociations(), "wiz_integration_aws_sns": resourceWizIntegrationAwsSNS(), "wiz_integration_servicenow": resourceWizIntegrationServiceNow(), diff --git a/internal/provider/resource_connector_aws.go b/internal/provider/resource_connector_aws.go index 9fbf516..372ea0c 100644 --- a/internal/provider/resource_connector_aws.go +++ b/internal/provider/resource_connector_aws.go @@ -137,23 +137,18 @@ func resourceWizConnectorAws() *schema.Resource { }, ), ), - CreateContext: resourceWizConnectorCreate, - ReadContext: resourceWizConnectorRead, - UpdateContext: resourceWizConnectorUpdate, - DeleteContext: resourceWizConnectorDelete, + CreateContext: resourceWizConnectorAwsCreate, + ReadContext: resourceWizConnectorAwsRead, + UpdateContext: resourceWizConnectorAwsUpdate, + DeleteContext: resourceWizConnectorAwsDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } -// CreateConnector struct -type CreateConnector struct { - CreateConnector wiz.CreateConnectorPayload `json:"createConnector"` -} - -func resourceWizConnectorCreate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { - tflog.Info(ctx, "resourceWizConnectorCreate called...") +func resourceWizConnectorAwsCreate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizConnectorAwsCreate called...") query := `mutation CreateConnector($input: CreateConnectorInput!) { createConnector(input: $input) { @@ -184,16 +179,11 @@ func resourceWizConnectorCreate(ctx context.Context, d *schema.ResourceData, m i // set the id d.SetId(data.CreateConnector.Connector.ID) - return resourceWizConnectorRead(ctx, d, m) -} - -// ReadConnectorPayload struct -type ReadConnectorPayload struct { - Connector wiz.Connector `json:"connector"` + return resourceWizConnectorAwsRead(ctx, d, m) } -func resourceWizConnectorRead(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { - tflog.Info(ctx, "resourceWizConnectorRead called...") +func resourceWizConnectorAwsRead(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizConnectorAwsRead called...") // check the id if d.Id() == "" { @@ -356,12 +346,7 @@ func resourceWizConnectorRead(ctx context.Context, d *schema.ResourceData, m int return diags } -// UpdateConnector struct -type UpdateConnector struct { - UpdateConnector wiz.UpdateConnectorPayload `json:"updateConnector"` -} - -func resourceWizConnectorUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { +func resourceWizConnectorAwsUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { tflog.Info(ctx, "resourceWizConnectorUpdate called...") // check the id @@ -404,15 +389,10 @@ func resourceWizConnectorUpdate(ctx context.Context, d *schema.ResourceData, m i return diags } - return resourceWizConnectorRead(ctx, d, m) -} - -// DeleteConnector struct -type DeleteConnector struct { - DeleteConnector wiz.DeleteConnectorPayload `json:"_stub"` + return resourceWizConnectorAwsRead(ctx, d, m) } -func resourceWizConnectorDelete(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { +func resourceWizConnectorAwsDelete(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { tflog.Info(ctx, "resourceWizConnectorDelete called...") // check the id diff --git a/internal/provider/resource_connector_gcp.go b/internal/provider/resource_connector_gcp.go new file mode 100644 index 0000000..89f400b --- /dev/null +++ b/internal/provider/resource_connector_gcp.go @@ -0,0 +1,479 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "wiz.io/hashicorp/terraform-provider-wiz/internal" + "wiz.io/hashicorp/terraform-provider-wiz/internal/client" + "wiz.io/hashicorp/terraform-provider-wiz/internal/utils" + "wiz.io/hashicorp/terraform-provider-wiz/internal/wiz" +) + +const ( + auditLogMonitorEnabledKey = "auditLogMonitorEnabled" + auditLogsConfigKey = "auditLogsConfig" + pubSubKey = "pub_sub" + projectIDKey = "project_id" + topicIDKey = "topic_id" + subscriptionIDKey = "subscription_id" + topicNameKey = "topicName" + subscriptionIDNameKey = "subscriptionID" +) + +func resourceWizConnectorGcp() *schema.Resource { + return &schema.Resource{ + Description: "Connectors are used to connect GCP resources to Wiz.", + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "Wiz internal identifier for the connector.", + Computed: true, + }, + "name": { + Type: schema.TypeString, + Description: "The connector name.", + Required: true, + }, + "enabled": { + Type: schema.TypeBool, + Description: "Whether the connector is enabled.", + Optional: true, + Default: true, + }, + "is_managed_identity": { + Type: schema.TypeString, + Description: "Is managed identity?", + Computed: true, + }, + "folder_id": { + Type: schema.TypeString, + Description: "The GCP folder ID.", + Computed: true, + }, + "organization_id": { + Type: schema.TypeString, + Description: "The GCP organization ID.", + Computed: true, + }, + "projects": { + Type: schema.TypeList, + Description: "The GCP projects to target with the connector.", + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "excluded_projects": { + Type: schema.TypeList, + Description: "The GCP projects excluded by the connector.", + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "included_folders": { + Type: schema.TypeList, + Description: "The GCP folders included by the connector.", + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "excluded_folders": { + Type: schema.TypeList, + Description: "The GCP folders excluded by the connector.", + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "audit_log_monitor_enabled": { + Type: schema.TypeBool, + Description: "Whether audit log monitor is enabled. Note an advanced license is required.", + Computed: true, + }, + "disk_analyzer_inflight_disabled": { + Type: schema.TypeBool, + Description: "If using Outpost, whether disk analyzer inflight scanning is disabled.", + Computed: true, + }, + "events_topic_name": { + Type: schema.TypeString, + Description: "If using Wiz Cloud Events, the Topic Name in format `projects//topics/`.", + Computed: true, + }, + "events_pub_sub_subscription_id": { + Type: schema.TypeString, + Description: "If using Wiz Cloud Events, the Pub/Sub Subscription ID.", + Computed: true, + }, + "auth_params": { + Type: schema.TypeString, + Description: "The authentication parameters. Must be represented in `JSON` format.", + Required: true, + Sensitive: true, + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringIsJSON, + ), + }, + "extra_config": { + // these are JSON fields; the schema does not support overrides, once a field is set, future changes require it to be passed + Type: schema.TypeString, + Description: "Extra configuration for the connector. Must be represented in `JSON` format.", + Optional: true, + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringIsJSON, + ), + }, + }, + // auth_params requires a resource recreation as they cannot be updated. + // to accommodate for importing resources into state, we can't use `ForceNew` in the schema definition. + // we use a customdiff and `ForceNewIfChange` for below attributes for only a change condition. + CustomizeDiff: customdiff.All( + customdiff.ForceNewIfChange("auth_params", func(ctx context.Context, old, new, meta any) bool { + if old.(string) != "" { + return old.(string) != new.(string) + } + return false + }, + ), + ), + CreateContext: resourceWizConnectorGcpCreate, + ReadContext: resourceWizConnectorGcpRead, + UpdateContext: resourceWizConnectorGcpUpdate, + DeleteContext: resourceWizConnectorGcpDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func resourceWizConnectorGcpCreate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizConnectorGcpCreate called...") + + query := `mutation CreateConnector($input: CreateConnectorInput!) { + createConnector(input: $input) { + connector { + id + } + } + } + ` + // populate the graphql variables + vars := &wiz.CreateConnectorInput{} + vars.Name = d.Get("name").(string) + enabled := d.Get("enabled").(bool) + vars.Type = "gcp" + vars.Enabled = &enabled + + vars.AuthParams = json.RawMessage(d.Get("auth_params").(string)) + vars.ExtraConfig = json.RawMessage(d.Get("extra_config").(string)) + + // process the request + data := &CreateConnector{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "connector", "create") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + // set the id + d.SetId(data.CreateConnector.Connector.ID) + + return resourceWizConnectorGcpRead(ctx, d, m) +} + +func resourceWizConnectorGcpRead(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizConnectorGcpRead called...") + + // check the id + if d.Id() == "" { + return nil + } + + // define the graphql query + query := `query GetConnector($id: ID!) { + connector(id: $id) { + id + name + enabled + authParams + extraConfig + config { + ... on ConnectorConfigGCP { + auditLogMonitorEnabled + diskAnalyzerInFlightDisabled + includedFolders + excludedFolders + excludedProjects + delegateUser + projects + organization_id + project_id + folder_id + auditLogMonitorEnabled + auditLogsConfig { + pub_sub { + topicName + subscriptionID + } + } + } + } + type { + ...ConnectorTypeFrag + } + } + } + + fragment ConnectorTypeFrag on ConnectorType { + id + name + authorizeUrls + } +` + // populate the graphql variables + vars := &internal.QueryVariables{} + vars.ID = d.Id() + + // process the request + data := &ReadConnectorPayload{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "connector", "read") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + tflog.Info(ctx, "Error from API call, checking if resource was deleted outside Terraform.") + tflog.Debug(ctx, fmt.Sprintf("Response: (%T) %s", data, utils.PrettyPrint(data))) + // we are checking if the response does not have an ID and if so we mark the resource as new + // once the vendor implements consistent and documented error handling we can + // move to an error handling factory to inspect definitive errors and handle accordingly + if data.Connector.ID == "" { + tflog.Info(ctx, "resource not found, marking as new.") + d.SetId("") + d.MarkNewResource() + return nil + } + return diags + } + + err := d.Set("name", data.Connector.Name) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("id", data.Connector.ID) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("enabled", data.Connector.Enabled) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + var connectorConfig wiz.ConnectorConfigGCP + connectorConfigBytes, err := json.Marshal(data.Connector.Config) + if err != nil { + return append(diags, diag.Errorf("unable to marshal ConnectorConfigGCP: %v", err)...) + } + if err := json.Unmarshal(connectorConfigBytes, &connectorConfig); err != nil { + return append(diags, diag.Errorf("unable to unmarshal ConnectorConfigGCP: %v", err)...) + } + + err = d.Set("projects", utils.ConvertSliceToGenericArray(connectorConfig.Projects)) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("disk_analyzer_inflight_disabled", connectorConfig.DiskAnalyzerInFlightDisabled) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("audit_log_monitor_enabled", connectorConfig.AuditLogMonitorEnabled) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("organization_id", connectorConfig.OrganizationID) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("folder_id", connectorConfig.FolderID) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("excluded_folders", utils.ConvertSliceToGenericArray(connectorConfig.ExcludedFolders)) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("included_folders", utils.ConvertSliceToGenericArray(connectorConfig.IncludedFolders)) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("excluded_projects", utils.ConvertSliceToGenericArray(connectorConfig.ExcludedProjects)) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("events_topic_name", connectorConfig.AuditLogsConfig.PubSub.TopicName) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("events_pub_sub_subscription_id", connectorConfig.AuditLogsConfig.PubSub.SubscriptionID) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + diagsExtraConfig := updateExtraConfig(ctx, *data, d, diags) + if diagsExtraConfig != nil { + return append(diags, diagsExtraConfig...) + } + + return diags +} + +func resourceWizConnectorGcpUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizConnectorGcpUpdate called...") + + // check the id + if d.Id() == "" { + return nil + } + + // define the graphql query + query := `mutation UpdateConnector($input: UpdateConnectorInput!) { + updateConnector(input: $input) { + connector { + id + name + enabled + extraConfig + } + } + }` + + // populate the graphql variables + vars := &wiz.UpdateConnectorInput{} + vars.ID = d.Id() + + if d.HasChange("name") { + vars.Patch.Name = d.Get("name").(string) + } + if d.HasChange("enabled") { + enabled := d.Get("enabled").(bool) + vars.Patch.Enabled = &enabled + } + if d.HasChange("extra_config") { + vars.Patch.ExtraConfig = json.RawMessage(d.Get("extra_config").(string)) + } + + // process the request + data := &UpdateConnector{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "connector", "update") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + return resourceWizConnectorGcpRead(ctx, d, m) +} + +func resourceWizConnectorGcpDelete(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizConnectorGcpDelete called...") + + // check the id + if d.Id() == "" { + return nil + } + + // define the graphql query + query := `mutation DeleteConnector($input: DeleteConnectorInput!) { + deleteConnector(input: $input) { + _stub + } + } + ` + // populate the graphql variables + vars := &wiz.DeleteConnectorInput{} + vars.ID = d.Id() + + // process the request + data := &UpdateUser{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "connector", "delete") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + return diags +} + +// Wiz API limitations prevent nullifying the `auditLogsConfig/pub_sub` field. For example, disabling `auditLogMonitorEnabled` +// will results in perpetual drift detection of extraConfig as the related pub_sub information will always be in the response once set. +// Furthermore, additional `pub_sub `fields require normalization and removal of unnecessary fields. +func updateExtraConfig(ctx context.Context, data ReadConnectorPayload, d *schema.ResourceData, diags diag.Diagnostics) diag.Diagnostics { + tflog.Info(ctx, "updateExtraConfig called...") + + var mapExtraConfig map[string]interface{} + err := json.Unmarshal(data.Connector.ExtraConfig, &mapExtraConfig) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + auditLogMonitoringEnabled, ok := mapExtraConfig[auditLogMonitorEnabledKey].(bool) + if !ok { + auditLogMonitoringEnabled = true + } + + if auditLogMonitoringEnabled { + if pubSubConfig, ok := mapExtraConfig[auditLogsConfigKey].(map[string]interface{})[pubSubKey].(map[string]interface{}); ok { + projectID, ok := pubSubConfig[projectIDKey].(string) + if !ok { + return addFieldError(diags, projectIDKey, pubSubKey) + } + topicID, ok := pubSubConfig[topicIDKey].(string) + if !ok { + return addFieldError(diags, topicIDKey, pubSubKey) + } + topicName := fmt.Sprintf("projects/%s/topics/%s", projectID, topicID) + subscriptionID, ok := pubSubConfig[subscriptionIDKey].(string) + if !ok { + return addFieldError(diags, subscriptionIDKey, pubSubKey) + } + + pubSubConfig[topicNameKey] = topicName + pubSubConfig[subscriptionIDNameKey] = subscriptionID + delete(pubSubConfig, projectIDKey) + delete(pubSubConfig, topicIDKey) + delete(pubSubConfig, subscriptionIDKey) + + mapExtraConfig[auditLogsConfigKey].(map[string]interface{})[pubSubKey] = pubSubConfig + + } + } else { + delete(mapExtraConfig, auditLogsConfigKey) + } + tflog.Debug(ctx, fmt.Sprintf("mapExtraConfig: %s", mapExtraConfig)) + + extraConfig, err := json.Marshal(mapExtraConfig) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + err = d.Set("extra_config", string(extraConfig)) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + return diags +} + +func addFieldError(diags diag.Diagnostics, fieldName, keyName string) diag.Diagnostics { + return append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "An issue was encountered while processing the `extraConfig` field.", + Detail: fmt.Sprintf("missing or invalid %s field in %s", fieldName, keyName), + }) +} diff --git a/internal/provider/resource_connector_gcp_test.go b/internal/provider/resource_connector_gcp_test.go new file mode 100644 index 0000000..525760a --- /dev/null +++ b/internal/provider/resource_connector_gcp_test.go @@ -0,0 +1,47 @@ +package provider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" +) + +var extraConfigErrorSummary = "Invalid extra configuration" + +func TestAddFieldError(t *testing.T) { + diags := diag.Diagnostics{} + fieldName := "foo" + keyName := "bar" + + // Test case 1: Invalid extra configuration + expectedDiags := diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "An issue was encountered while processing the `extraConfig` field.", + Detail: "missing or invalid foo field in bar", + }, + } + + extraConfigErrorSummary = "Invalid extra configuration" + actualDiags := addFieldError(diags, fieldName, keyName) + + if len(actualDiags) != len(expectedDiags) { + t.Errorf("Expected %d diagnostics, but got %d", len(expectedDiags), len(actualDiags)) + } + + for i, actualDiag := range actualDiags { + expectedDiag := expectedDiags[i] + + if actualDiag.Severity != expectedDiag.Severity { + t.Errorf("Expected severity %v, but got %v", expectedDiag.Severity, actualDiag.Severity) + } + + if actualDiag.Summary != expectedDiag.Summary { + t.Errorf("Expected summary %q, but got %q", expectedDiag.Summary, actualDiag.Summary) + } + + if actualDiag.Detail != expectedDiag.Detail { + t.Errorf("Expected detail %q, but got %q", expectedDiag.Detail, actualDiag.Detail) + } + } +} diff --git a/internal/wiz/structs.go b/internal/wiz/structs.go index be9b8bc..eee05d0 100644 --- a/internal/wiz/structs.go +++ b/internal/wiz/structs.go @@ -598,6 +598,42 @@ type OutpostAWSConfig struct { DisableNatGateway bool `json:"disableNatGateway,omitempty"` } +// ConnectorConfigGCP struct -- updates +type ConnectorConfigGCP struct { + AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"` + AuthURI string `json:"auth_uri"` + AuditLogMonitorEnabled bool `json:"auditLogMonitorEnabled"` + AuditLogsConfig ConnectorConfigGCPAuditLogs `json:"auditLogsConfig"` + ClientEmail string `json:"client_email"` + ClientID string `json:"client_id"` + ClientX509CertURL string `json:"client_x509_cert_url"` + DelegateUser string `json:"delegateUser"` + DiskAnalyzerInFlightDisabled bool `json:"diskAnalyzerInFlightDisabled"` + ExcludedFolders []string `json:"excludedFolders"` + ExcludedProjects []string `json:"excludedProjects"` + FolderID string `json:"folder_id"` + IncludedFolders []string `json:"includedFolders"` + IsManagedIdentity bool `json:"isManagedIdentity"` + OrganizationID string `json:"organization_id"` + PrivateKey string `json:"private_key"` + PrivateKeyID string `json:"private_key_id"` + ProjectID string `json:"project_id"` + Projects []string `json:"projects"` + TokenURI string `json:"token_uri"` + Type string `json:"type"` +} + +// ConnectorConfigGCPAuditLogs struct -- updates +type ConnectorConfigGCPAuditLogs struct { + PubSub ConnectorConfigGCPPubSub `json:"pub_sub"` +} + +// ConnectorConfigGCPPubSub struct -- updates +type ConnectorConfigGCPPubSub struct { + SubscriptionID string `json:"subscriptionID"` + TopicName string `json:"topicName"` +} + // ConnectorConfigAWS struct -- updates type ConnectorConfigAWS struct { CustomerRoleARN string `json:"customerRoleARN"`