diff --git a/docs/resources/integration_servicenow.md b/docs/resources/integration_servicenow.md new file mode 100644 index 0000000..31b1199 --- /dev/null +++ b/docs/resources/integration_servicenow.md @@ -0,0 +1,53 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "wiz_integration_servicenow Resource - terraform-provider-wiz" +subcategory: "" +description: |- + Integrations are reusable, generic connections between Wiz and third-party platforms like Slack, Google Chat, and Jira that allow data from Wiz to be passed to your preferred tool. +--- + +# wiz_integration_servicenow (Resource) + +Integrations are reusable, generic connections between Wiz and third-party platforms like Slack, Google Chat, and Jira that allow data from Wiz to be passed to your preferred tool. + +## Example Usage + +```terraform +resource "wiz_integration_servicenow" "default" { + name = "default" + servicenow_url = var.servicename_url + servicenow_username = var.servicenow_username + servicenow_password = var.servicenow_password + scope = "All Resources, Restrict this Integration to global roles only" +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the integration. +- `servicenow_password` (String, Sensitive) ServiceNow password. (default: none, environment variable: WIZ_INTEGRATION_SERVICENOW_PASSWORD) +- `servicenow_url` (String) ServiceNow URL. (default: none, environment variable: WIZ_INTEGRATION_SERVICENOW_URL) +- `servicenow_username` (String) Email of a ServiceNow user with permissions to create tickets. (default: none, environment variable: WIZ_INTEGRATION_SERVICENOW_USERNAME) + +### Optional + +- `project_id` (String) The project this action is scoped to. +- `scope` (String) Scoping to a selected Project makes this Integration accessible only to users with global roles or Project-scoped access to the selected Project. Other users will not be able to see it, use it, or view its results. Integrations restricted to global roles cannot be seen or used by users with Project-scoped roles. + - Allowed values: + - Selected Project + - All Resources + - All Resources, Restrict this Integration to global roles only + + - Defaults to `All Resources, Restrict this Integration to global roles only`. +- `servicenow_client_id` (String) ServiceNow OAuth Client ID. (default: none, environment variable: WIZ_INTEGRATION_SERVICENOW_CLIENT_ID) +- `servicenow_client_secret` (String, Sensitive) ServiceNow OAuth Client Secret. (default: none, environment variable: WIZ_INTEGRATION_SERVICENOW_CLIENT_SECRET) + +### Read-Only + +- `created_at` (String) Identifies the date and time when the object was created. +- `id` (String) Identifier for this object. + + diff --git a/examples/resources/wiz_integration_servicenow/resource.tf b/examples/resources/wiz_integration_servicenow/resource.tf new file mode 100644 index 0000000..3038cb8 --- /dev/null +++ b/examples/resources/wiz_integration_servicenow/resource.tf @@ -0,0 +1,7 @@ +resource "wiz_integration_servicenow" "default" { + name = "default" + servicenow_url = var.servicename_url + servicenow_username = var.servicenow_username + servicenow_password = var.servicenow_password + scope = "All Resources, Restrict this Integration to global roles only" +} diff --git a/internal/acceptance/provider_test.go b/internal/acceptance/provider_test.go index cb11800..3fc4eea 100644 --- a/internal/acceptance/provider_test.go +++ b/internal/acceptance/provider_test.go @@ -38,3 +38,27 @@ func testAccPreCheck(t *testing.T) { t.Fatal("WIZ_AUTH_CLIENT_SECRET must be set for acceptance tests") } } + +func testAccPreCheckIntegrationServiceNow(t *testing.T) { + // You can add code here to run prior to any test case execution, for example assertions + // about the appropriate environment variables being set are common to see in a pre-check + // function. + if v := os.Getenv("WIZ_URL"); v == "" { + t.Fatal("WIZ_URL must be set for acceptance tests") + } + if v := os.Getenv("WIZ_AUTH_CLIENT_ID"); v == "" { + t.Fatal("WIZ_AUTH_CLIENT_ID must be set for acceptance tests") + } + if v := os.Getenv("WIZ_AUTH_CLIENT_SECRET"); v == "" { + t.Fatal("WIZ_AUTH_CLIENT_SECRET must be set for acceptance tests") + } + if v := os.Getenv("WIZ_INTEGRATION_SERVICENOW_URL"); v == "" { + t.Fatal("WIZ_INTEGRATION_SERVICENOW_URL must be set for wiz_integration_servicenow acceptance tests") + } + if v := os.Getenv("WIZ_INTEGRATION_SERVICENOW_USERNAME"); v == "" { + t.Fatal("WIZ_INTEGRATION_SERVICENOW_USERNAME must be set for wiz_integration_servicenow acceptance tests") + } + if v := os.Getenv("WIZ_INTEGRATION_SERVICENOW_PASSWORD"); v == "" { + t.Fatal("WIZ_INTEGRATION_SERVICENOW_PASSWORD must be set for wiz_integration_servicenow acceptance tests") + } +} diff --git a/internal/acceptance/resource_integration_servicenow_test.go b/internal/acceptance/resource_integration_servicenow_test.go new file mode 100644 index 0000000..049af74 --- /dev/null +++ b/internal/acceptance/resource_integration_servicenow_test.go @@ -0,0 +1,55 @@ +package acceptance + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccResourceWizIntegrationServiceNow_basic(t *testing.T) { + rName := acctest.RandomWithPrefix(ResourcePrefix) + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckIntegrationServiceNow(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testResourceWizIntegrationServiceNowBasic(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "wiz_integration_servicenow.foo", + "name", + rName, + ), + resource.TestCheckResourceAttr( + "wiz_integration_servicenow.foo", + "servicenow_url", + os.Getenv("WIZ_INTEGRATION_SERVICENOW_URL"), + ), + resource.TestCheckResourceAttr( + "wiz_integration_servicenow.foo", + "servicenow_username", + os.Getenv("WIZ_INTEGRATION_SERVICENOW_USERNAME"), + ), + resource.TestCheckResourceAttr( + "wiz_integration_servicenow.foo", + "scope", + "All Resources, Restrict this Integration to global roles only", + ), + ), + }, + }, + }) +} + +func testResourceWizIntegrationServiceNowBasic(rName string) string { + return fmt.Sprintf(` +resource "wiz_integration_servicenow" "foo" { + name = "%s" + scope = "All Resources, Restrict this Integration to global roles only" +} +`, rName) +} diff --git a/internal/acceptance/resource_saml_idp_test.go b/internal/acceptance/resource_saml_idp_test.go index b7ed1d7..e7505b9 100644 --- a/internal/acceptance/resource_saml_idp_test.go +++ b/internal/acceptance/resource_saml_idp_test.go @@ -16,7 +16,7 @@ func TestAccResourceWizSAMLIdp_basic(t *testing.T) { ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: TestAccResourceWizSAMLIdp_basic(rName), + Config: testResourceWizSAMLIdpBasic(rName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( "wiz_saml_idp.test", @@ -54,7 +54,7 @@ func TestAccResourceWizSAMLIdp_basic(t *testing.T) { }) } -func TestAccResourceWizSAMLIdp_basic(rName string) string { +func testResourceWizSAMLIdpBasic(rName string) string { return fmt.Sprintf(` resource "wiz_saml_idp" "test" { name = "%s" diff --git a/internal/common.go b/internal/common.go index 0f5bcea..8a20bee 100644 --- a/internal/common.go +++ b/internal/common.go @@ -155,3 +155,8 @@ var IntegrationScope = []string{ "All Resources", "All Resources, Restrict this Integration to global roles only", } + +// ProviderServiceNowAuthorizationType is used to infer the type of Authorization struct used in wiz.ServiceNowIntegrationParams +type ProviderServiceNowAuthorizationType struct { + Type string `json:"type"` +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 56efcc9..a77ac35 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -266,6 +266,7 @@ yLyKQXhw2W2Xs0qLeC1etA+jTGDK4UfLeC0SF7FSi8o5LL21L8IzApar2pR/ "wiz_control_associations": resourceWizControlAssociations(), "wiz_host_config_rule_associations": resourceWizHostConfigRuleAssociations(), "wiz_integration_aws_sns": resourceWizIntegrationAwsSNS(), + "wiz_integration_servicenow": resourceWizIntegrationServiceNow(), "wiz_project": resourceWizProject(), "wiz_saml_idp": resourceWizSAMLIdP(), "wiz_security_framework": resourceWizSecurityFramework(), diff --git a/internal/provider/resource_integration_servicenow.go b/internal/provider/resource_integration_servicenow.go new file mode 100644 index 0000000..0eff587 --- /dev/null +++ b/internal/provider/resource_integration_servicenow.go @@ -0,0 +1,317 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "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" +) + +func resourceWizIntegrationServiceNow() *schema.Resource { + return &schema.Resource{ + Description: "Integrations are reusable, generic connections between Wiz and third-party platforms like Slack, Google Chat, and Jira that allow data from Wiz to be passed to your preferred tool.", + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "Identifier for this object.", + Computed: true, + }, + "name": { + Type: schema.TypeString, + Description: "The name of the integration.", + Required: true, + }, + "created_at": { + Type: schema.TypeString, + Description: "Identifies the date and time when the object was created.", + Computed: true, + }, + "project_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The project this action is scoped to.", + }, + "scope": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "All Resources, Restrict this Integration to global roles only", + Description: fmt.Sprintf( + "Scoping to a selected Project makes this Integration accessible only to users with global roles or Project-scoped access to the selected Project. Other users will not be able to see it, use it, or view its results. Integrations restricted to global roles cannot be seen or used by users with Project-scoped roles. \n - Allowed values: %s", + utils.SliceOfStringToMDUList( + internal.IntegrationScope, + ), + ), + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringInSlice( + internal.IntegrationScope, + false, + ), + ), + }, + "servicenow_url": { + Type: schema.TypeString, + Required: true, + Description: "ServiceNow URL. (default: none, environment variable: WIZ_INTEGRATION_SERVICENOW_URL)", + DefaultFunc: schema.EnvDefaultFunc( + "WIZ_INTEGRATION_SERVICENOW_URL", + nil, + ), + }, + "servicenow_username": { + Type: schema.TypeString, + Required: true, + Description: "Email of a ServiceNow user with permissions to create tickets. (default: none, environment variable: WIZ_INTEGRATION_SERVICENOW_USERNAME)", + DefaultFunc: schema.EnvDefaultFunc( + "WIZ_INTEGRATION_SERVICENOW_USERNAME", + nil, + ), + }, + "servicenow_password": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "ServiceNow password. (default: none, environment variable: WIZ_INTEGRATION_SERVICENOW_PASSWORD)", + DefaultFunc: schema.EnvDefaultFunc( + "WIZ_INTEGRATION_SERVICENOW_PASSWORD", + nil, + ), + }, + "servicenow_client_id": { + Type: schema.TypeString, + Optional: true, + Description: "ServiceNow OAuth Client ID. (default: none, environment variable: WIZ_INTEGRATION_SERVICENOW_CLIENT_ID)", + DefaultFunc: schema.EnvDefaultFunc( + "WIZ_INTEGRATION_SERVICENOW_CLIENT_ID", + nil, + ), + }, + "servicenow_client_secret": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "ServiceNow OAuth Client Secret. (default: none, environment variable: WIZ_INTEGRATION_SERVICENOW_CLIENT_SECRET)", + DefaultFunc: schema.EnvDefaultFunc( + "WIZ_INTEGRATION_SERVICENOW_CLIENT_SECRET", + nil, + ), + }, + }, + CreateContext: resourceWizIntegrationAwsServiceNowCreate, + ReadContext: resourceWizIntegrationAwsServiceNowRead, + UpdateContext: resourceWizIntegrationAwsServiceNowUpdate, + DeleteContext: resourceWizIntegrationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func resourceWizIntegrationAwsServiceNowCreate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizIntegrationAwsServiceNowCreate called...") + + // define the graphql query + query := `mutation CreateIntegration($input: CreateIntegrationInput!) { + createIntegration( + input: $input + ) { + integration { + id + } + } + }` + + vars := &wiz.CreateIntegrationInput{} + vars.Name = d.Get("name").(string) + vars.Type = "SERVICE_NOW" + vars.ProjectID = d.Get("project_id").(string) + vars.IsAccessibleToAllProjects = convertIntegrationScopeToBool(d.Get("scope").(string)) + vars.Params.ServiceNow = &wiz.CreateServiceNowIntegrationParamsInput{} + vars.Params.ServiceNow.URL = d.Get("servicenow_url").(string) + vars.Params.ServiceNow.Authorization.Username = d.Get("servicenow_username").(string) + vars.Params.ServiceNow.Authorization.Password = d.Get("servicenow_password").(string) + vars.Params.ServiceNow.Authorization.ClientID = d.Get("servicenow_client_id").(string) + vars.Params.ServiceNow.Authorization.ClientSecret = d.Get("servicenow_client_secret").(string) + + // process the request + data := &CreateIntegration{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "integration_servicenow", "create") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + // set the id + d.SetId(data.CreateIntegration.Integration.ID) + + return resourceWizIntegrationAwsServiceNowRead(ctx, d, m) +} + +func resourceWizIntegrationAwsServiceNowRead(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizIntegrationAwsServiceNowRead called...") + + // check the id + if d.Id() == "" { + return nil + } + + // define the graphql query + query := `query integration ( + $id: ID! + ) { + integration( + id: $id + ) { + id + name + createdAt + updatedAt + project { + id + } + type + isAccessibleToAllProjects + usedByRules { + id + } + paramsType: params { + type: __typename + } + params { + ... on ServiceNowIntegrationParams { + url + authorizationType: authorization { + type: __typename + } + authorization { + ... on ServiceNowIntegrationBasicAuthorization { + password + username + } + ... on ServiceNowIntegrationOAuthAuthorization { + password + username + clientId + clientSecret + } + } + } + } + } + }` + + // populate the graphql variables + vars := &internal.QueryVariables{} + vars.ID = d.Id() + + // process the request + data := &ReadIntegrationPayload{} + params := &wiz.ServiceNowIntegrationParams{} + data.Integration.Params = params + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "integration_servicenow", "read") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + tflog.Info(ctx, "Error from API call, checking if resource was deleted outside Terraform.") + if data.Integration.ID == "" { + tflog.Debug(ctx, fmt.Sprintf("Response: (%T) %s", data, utils.PrettyPrint(data))) + tflog.Info(ctx, "Resource not found, marking as new.") + d.SetId("") + d.MarkNewResource() + return nil + } + return diags + } + + // set the resource parameters + err := d.Set("name", data.Integration.Name) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("created_at", data.Integration.CreatedAt) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("project_id", data.Integration.Project.ID) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("servicenow_url", params.URL) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("servicenow_username", params.Authorization.(map[string]interface{})["username"]) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("servicenow_password", d.Get("servicenow_password").(string)) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + // determine credentials type and populate resource data + tflog.Debug(ctx, fmt.Sprintf("params.AuthorizationType.Type %s (%T)", params.AuthorizationType.Type, params.Authorization)) + + switch params.AuthorizationType.Type { + case "ServiceNowIntegrationOAuthAuthorization": + err = d.Set("servicenow_client_id", params.Authorization.(map[string]interface{})["servicenow_client_id"]) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("servicenow_client_secret", d.Get("servicenow_client_secret").(string)) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + } + + return diags +} + +func resourceWizIntegrationAwsServiceNowUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizIntegrationAwsServiceNowUpdate called...") + + // check the id + if d.Id() == "" { + return nil + } + + // define the graphql query + query := `mutation UpdateIntegration( + $input: UpdateIntegrationInput! + ) { + updateIntegration(input: $input) { + integration { + id + } + } + }` + + // populate the graphql variables + vars := &wiz.UpdateIntegrationInput{} + vars.ID = d.Id() + vars.Patch.Name = d.Get("name").(string) + vars.Patch.Params.ServiceNow = &wiz.UpdateServiceNowIntegrationParamsInput{} + vars.Patch.Params.ServiceNow.URL = d.Get("servicenow_url").(string) + vars.Patch.Params.ServiceNow.Authorization.ClientID = d.Get("servicenow_client_id").(string) + vars.Patch.Params.ServiceNow.Authorization.ClientSecret = d.Get("servicenow_client_secret").(string) + vars.Patch.Params.ServiceNow.Authorization.Username = d.Get("servicenow_username").(string) + vars.Patch.Params.ServiceNow.Authorization.Password = d.Get("servicenow_password").(string) + + // process the request + data := &UpdateIntegration{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "integration_servicenow", "update") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + return diags +} diff --git a/internal/wiz/structs.go b/internal/wiz/structs.go index bd58774..fbae29b 100644 --- a/internal/wiz/structs.go +++ b/internal/wiz/structs.go @@ -1849,9 +1849,11 @@ type PagerDutyIntegrationParams struct { } // ServiceNowIntegrationParams struct +// AuthorizationType is a provider defined field used to determine how to handle the response type ServiceNowIntegrationParams struct { - Authorization interface{} `json:"authorization"` - URL string `json:"url"` + Authorization interface{} `json:"authorization"` + AuthorizationType internal.ProviderServiceNowAuthorizationType `json:"authorizationType"` + URL string `json:"url"` } // WebhookIntegrationParams struct