From 8c599e8a89ecf4987b3ce455188fa4add0ecc826 Mon Sep 17 00:00:00 2001 From: cholesterol Date: Mon, 24 Jul 2023 10:16:31 -0400 Subject: [PATCH] add jira integration --- docs/resources/integration_jira.md | 57 +++ .../wiz_integration_jira/resource.tf | 7 + internal/acceptance/common.go | 2 + internal/acceptance/provider_test.go | 2 + .../resource_integration_jira_test.go | 55 +++ internal/provider/provider.go | 1 + .../provider/resource_integration_jira.go | 357 ++++++++++++++++++ internal/wiz/structs.go | 2 +- 8 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 docs/resources/integration_jira.md create mode 100644 examples/resources/wiz_integration_jira/resource.tf create mode 100644 internal/acceptance/resource_integration_jira_test.go create mode 100644 internal/provider/resource_integration_jira.go diff --git a/docs/resources/integration_jira.md b/docs/resources/integration_jira.md new file mode 100644 index 0000000..8df0d59 --- /dev/null +++ b/docs/resources/integration_jira.md @@ -0,0 +1,57 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "wiz_integration_jira 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_jira (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_jira" "default" { + name = "default" + jira_url = var.jira_url + jira_username = var.jira_username + jira_password = var.jira_password + scope = "All Resources, Restrict this Integration to global roles only" +} +``` + + +## Schema + +### Required + +- `jira_url` (String) Jira URL. (default: none, environment variable: WIZ_INTEGRATION_JIRA_URL) +- `name` (String) The name of the integration. + +### Optional + +- `jira_allow_insecure_tls` (Boolean) Jira integration TLS setting +- `jira_client_certificate_and_private_key` (String, Sensitive) Jira PEM with client certificate and private key +- `jira_is_on_prem` (Boolean) Whether Jira instance is on prem + - Defaults to `false`. +- `jira_password` (String, Sensitive) Jira password. (default: none, environment variable: WIZ_INTEGRATION_JIRA_PASSWORD) +- `jira_pat` (String, Sensitive) Jira personal access token (used for on-prem). (default: none, environment variable: WIZ_INTEGRATION_JIRA_PAT) +- `jira_server_ca` (String) Jira server CA +- `jira_server_type` (String) Jira server type + - Defaults to `CLOUD`. +- `jira_username` (String) Email of a Jira user with permissions to create tickets. (default: none, environment variable: WIZ_INTEGRATION_JIRA_USERNAME) +- `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`. + +### 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_jira/resource.tf b/examples/resources/wiz_integration_jira/resource.tf new file mode 100644 index 0000000..527ffab --- /dev/null +++ b/examples/resources/wiz_integration_jira/resource.tf @@ -0,0 +1,7 @@ +resource "wiz_integration_jira" "default" { + name = "default" + jira_url = var.jira_url + jira_username = var.jira_username + jira_password = var.jira_password + scope = "All Resources, Restrict this Integration to global roles only" +} diff --git a/internal/acceptance/common.go b/internal/acceptance/common.go index 6dc58d0..07aad18 100644 --- a/internal/acceptance/common.go +++ b/internal/acceptance/common.go @@ -15,6 +15,8 @@ const ( TcUser TestCase = "USER" // TcServiceNow test case TcServiceNow TestCase = "SERVICE_NOW" + // TcJira test case + TcJira TestCase = "JIRA" // TcSubscriptionResourceGroups test case TcSubscriptionResourceGroups TestCase = "SUBSCRIPTION_RESOURCE_GROUPS" ) diff --git a/internal/acceptance/provider_test.go b/internal/acceptance/provider_test.go index 03310b7..6d0d9bd 100644 --- a/internal/acceptance/provider_test.go +++ b/internal/acceptance/provider_test.go @@ -37,6 +37,8 @@ func testAccPreCheck(t *testing.T, tc TestCase) { envVars = append(commonEnvVars, "WIZ_SMTP_DOMAIN") case TcServiceNow: envVars = append(commonEnvVars, "WIZ_INTEGRATION_SERVICENOW_URL", "WIZ_INTEGRATION_SERVICENOW_USERNAME", "WIZ_INTEGRATION_SERVICENOW_PASSWORD") + case TcJira: + envVars = append(commonEnvVars, "WIZ_INTEGRATION_JIRA_URL", "WIZ_INTEGRATION_JIRA_USERNAME", "WIZ_INTEGRATION_JIRA_PASSWORD", "WIZ_INTEGRATION_JIRA_PROJECT") case TcSubscriptionResourceGroups: envVars = append(commonEnvVars, "WIZ_SUBSCRIPTION_ID") default: diff --git a/internal/acceptance/resource_integration_jira_test.go b/internal/acceptance/resource_integration_jira_test.go new file mode 100644 index 0000000..0ea152f --- /dev/null +++ b/internal/acceptance/resource_integration_jira_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 TestAccResourceWizIntegrationJira_basic(t *testing.T) { + rName := acctest.RandomWithPrefix(ResourcePrefix) + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t, TcJira) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testResourceWizIntegrationJiraBasic(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "wiz_integration_jira.foo", + "name", + rName, + ), + resource.TestCheckResourceAttr( + "wiz_integration_jira.foo", + "jira_url", + os.Getenv("WIZ_INTEGRATION_JIRA_URL"), + ), + resource.TestCheckResourceAttr( + "wiz_integration_jira.foo", + "jira_username", + os.Getenv("WIZ_INTEGRATION_JIRA_USERNAME"), + ), + resource.TestCheckResourceAttr( + "wiz_integration_jira.foo", + "scope", + "All Resources, Restrict this Integration to global roles only", + ), + ), + }, + }, + }) +} + +func testResourceWizIntegrationJiraBasic(rName string) string { + return fmt.Sprintf(` +resource "wiz_integration_jira" "foo" { + name = "%s" + scope = "All Resources, Restrict this Integration to global roles only" +} +`, rName) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index b56a067..c9bd56e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -270,6 +270,7 @@ yLyKQXhw2W2Xs0qLeC1etA+jTGDK4UfLeC0SF7FSi8o5LL21L8IzApar2pR/ "wiz_host_config_rule_associations": resourceWizHostConfigRuleAssociations(), "wiz_integration_aws_sns": resourceWizIntegrationAwsSNS(), "wiz_integration_servicenow": resourceWizIntegrationServiceNow(), + "wiz_integration_jira": resourceWizIntegrationJira(), "wiz_project": resourceWizProject(), "wiz_saml_idp": resourceWizSAMLIdP(), "wiz_security_framework": resourceWizSecurityFramework(), diff --git a/internal/provider/resource_integration_jira.go b/internal/provider/resource_integration_jira.go new file mode 100644 index 0000000..8688a88 --- /dev/null +++ b/internal/provider/resource_integration_jira.go @@ -0,0 +1,357 @@ +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 resourceWizIntegrationJira() *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, + ), + ), + }, + "jira_url": { + Type: schema.TypeString, + Required: true, + Description: "Jira URL. (default: none, environment variable: WIZ_INTEGRATION_JIRA_URL)", + DefaultFunc: schema.EnvDefaultFunc( + "WIZ_INTEGRATION_JIRA_URL", + nil, + ), + }, + "jira_server_type": { + Type: schema.TypeString, + Optional: true, + Description: "Jira server type", + Default: "CLOUD", + }, + "jira_is_on_prem": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether Jira instance is on prem", + Default: false, + }, + "jira_allow_insecure_tls": { + Type: schema.TypeBool, + Optional: true, + Description: "Jira integration TLS setting", + }, + "jira_server_ca": { + Type: schema.TypeString, + Optional: true, + Description: "Jira server CA", + }, + "jira_client_certificate_and_private_key": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Jira PEM with client certificate and private key", + }, + "jira_username": { + Type: schema.TypeString, + Optional: true, + Description: "Email of a Jira user with permissions to create tickets. (default: none, environment variable: WIZ_INTEGRATION_JIRA_USERNAME)", + DefaultFunc: schema.EnvDefaultFunc( + "WIZ_INTEGRATION_JIRA_USERNAME", + nil, + ), + }, + "jira_password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Jira password. (default: none, environment variable: WIZ_INTEGRATION_JIRA_PASSWORD)", + DefaultFunc: schema.EnvDefaultFunc( + "WIZ_INTEGRATION_JIRA_PASSWORD", + nil, + ), + }, + "jira_pat": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Jira personal access token (used for on-prem). (default: none, environment variable: WIZ_INTEGRATION_JIRA_PAT)", + DefaultFunc: schema.EnvDefaultFunc( + "WIZ_INTEGRATION_JIRA_PAT", + nil, + ), + }, + }, + CreateContext: resourceWizIntegrationJiraCreate, + ReadContext: resourceWizIntegrationJiraRead, + UpdateContext: resourceWizIntegrationJiraUpdate, + DeleteContext: resourceWizIntegrationDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func resourceWizIntegrationJiraCreate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizIntegrationJiraCreate 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 = "JIRA" + vars.ProjectID = d.Get("project_id").(string) + vars.IsAccessibleToAllProjects = convertIntegrationScopeToBool(d.Get("scope").(string)) + vars.Params.Jira = &wiz.CreateJiraIntegrationParamsInput{} + vars.Params.Jira.ServerURL = d.Get("jira_url").(string) + vars.Params.Jira.ServerType = d.Get("jira_server_type").(string) + vars.Params.Jira.IsOnPrem = d.Get("jira_is_on_prem").(bool) + vars.Params.Jira.TLSConfig.AllowInsecureTLS = utils.ConvertBoolToPointer(d.Get("jira_allow_insecure_tls").(bool)) + vars.Params.Jira.TLSConfig.ClientCertificateAndPrivateKey = d.Get("jira_client_certificate_and_private_key").(string) + vars.Params.Jira.TLSConfig.ServerCA = d.Get("jira_server_ca").(string) + vars.Params.Jira.Authorization.Username = d.Get("jira_username").(string) + vars.Params.Jira.Authorization.Password = d.Get("jira_password").(string) + vars.Params.Jira.Authorization.PersonalAccessToken = d.Get("jira_pat").(string) + + // process the request + data := &CreateIntegration{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "integration_jira", "create") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + // set the id + d.SetId(data.CreateIntegration.Integration.ID) + + return resourceWizIntegrationJiraRead(ctx, d, m) +} + +func resourceWizIntegrationJiraRead(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizIntegrationJiraRead 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 JiraIntegrationParams { + url + serverType + onPremConfig { + isOnPrem + } + tlsConfig { + allowInsecureTLS + serverCA + clientCertificateAndPrivateKey + } + authorization { + ... on JiraIntegrationBasicAuthorization { + password + username + } + ... on JiraIntegrationTokenBearerAuthorization { + token + } + } + } + } + } + }` + + // populate the graphql variables + vars := &internal.QueryVariables{} + vars.ID = d.Id() + + // process the request + data := &ReadIntegrationPayload{} + params := &wiz.JiraIntegrationParams{} + data.Integration.Params = params + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "integration_jira", "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("jira_url", params.URL) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("jira_server_type", params.ServerType) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("jira_is_on_prem", params.OnPremConfig.IsOnPrem) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("jira_allow_insecure_tls", params.TLSConfig.AllowInsecureTLS) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("jira_server_ca", params.TLSConfig.ServerCA) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("jira_client_certificate_and_private_key", params.TLSConfig.ClientCertificateAndPrivateKey) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("jira_username", params.Authorization.(map[string]interface{})["username"]) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("jira_password", d.Get("jira_password").(string)) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + err = d.Set("jira_pat", d.Get("jira_pat").(string)) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + return diags +} + +func resourceWizIntegrationJiraUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizIntegrationJiraUpdate 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.Jira = &wiz.UpdateJiraIntegrationParamsInput{} + vars.Patch.Params.Jira.ServerURL = d.Get("jira_url").(string) + vars.Patch.Params.Jira.ServerType = d.Get("jira_server_type").(string) + vars.Patch.Params.Jira.IsOnPrem = utils.ConvertBoolToPointer(d.Get("jira_is_on_prem").(bool)) + vars.Patch.Params.Jira.TLSConfig.AllowInsecureTLS = utils.ConvertBoolToPointer(d.Get("jira_allow_insecure_tls").(bool)) + vars.Patch.Params.Jira.TLSConfig.ServerCA = d.Get("jira_server_ca").(string) + vars.Patch.Params.Jira.TLSConfig.ClientCertificateAndPrivateKey = d.Get("jira_client_certificate_and_private_key").(string) + vars.Patch.Params.Jira.Authorization.Username = d.Get("jira_username").(string) + vars.Patch.Params.Jira.Authorization.Password = d.Get("jira_password").(string) + vars.Patch.Params.Jira.Authorization.PersonalAccessToken = d.Get("jira_pat").(string) + + // process the request + data := &UpdateIntegration{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "integration_jira", "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 d33fd5c..66c0232 100644 --- a/internal/wiz/structs.go +++ b/internal/wiz/structs.go @@ -2296,7 +2296,7 @@ type PagerDutyActionCreateIncidentTemplateParams struct { // JiraActionCreateTicketTemplateParams struct type JiraActionCreateTicketTemplateParams struct { - Fields JiraTicketFields `json:"filds"` + Fields JiraTicketFields `json:"fields"` } // JiraActionAddCommentTemplateParams struct