diff --git a/examples/repository_security_and_analysis/README.md b/examples/repository_security_and_analysis/README.md new file mode 100644 index 0000000000..074f68e824 --- /dev/null +++ b/examples/repository_security_and_analysis/README.md @@ -0,0 +1,18 @@ +# Repository Visibility Example + +This demos setting `security_and_analysis` for a repository. See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-security-and-analysis-settings-for-your-repository for details on what these settings do. + +This example will create a repositories in the specified `owner` organization. See https://www.terraform.io/docs/providers/github/index.html for details on configuring [`providers.tf`](./providers.tf) accordingly. + +Alternatively, you may use variables passed via command line: + +```console +export GITHUB_OWNER= +export GITHUB_TOKEN= +``` + +```console +terraform apply \ + -var "owner=${GITHUB_OWNER}" \ + -var "github_token=${GITHUB_TOKEN}" +``` \ No newline at end of file diff --git a/examples/repository_security_and_analysis/main.tf b/examples/repository_security_and_analysis/main.tf new file mode 100644 index 0000000000..8cdbb72452 --- /dev/null +++ b/examples/repository_security_and_analysis/main.tf @@ -0,0 +1,19 @@ +resource "github_repository" "terraformed" { + name = "terraformed" + description = "A repository created by terraform" + visibility = "public" + + security_and_analysis { + # Cannot set advanced_security for public repositories as it is always on by default. + # advanced_security { + # status = "enabled" + # } + secret_scanning { + status = "enabled" + } + secret_scanning_push_protection { + status = "enabled" + } + } +} + diff --git a/examples/repository_security_and_analysis/outputs.tf b/examples/repository_security_and_analysis/outputs.tf new file mode 100644 index 0000000000..4f51a375c4 --- /dev/null +++ b/examples/repository_security_and_analysis/outputs.tf @@ -0,0 +1,4 @@ +output "repository" { + description = "Example repository JSON blob" + value = github_repository.terraformed +} diff --git a/examples/repository_security_and_analysis/providers.tf b/examples/repository_security_and_analysis/providers.tf new file mode 100644 index 0000000000..6a02f75e29 --- /dev/null +++ b/examples/repository_security_and_analysis/providers.tf @@ -0,0 +1,12 @@ +provider "github" { + owner = var.owner + token = var.github_token +} + +terraform { + required_providers { + github = { + source = "integrations/github" + } + } +} \ No newline at end of file diff --git a/examples/repository_security_and_analysis/variables.tf b/examples/repository_security_and_analysis/variables.tf new file mode 100644 index 0000000000..aafb16bf9f --- /dev/null +++ b/examples/repository_security_and_analysis/variables.tf @@ -0,0 +1,9 @@ +variable "owner" { + description = "GitHub owner used to configure the provider" + type = string +} + +variable "github_token" { + description = "GitHub access token used to configure the provider" + type = string +} diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index 468013f890..ce3d32e26b 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -60,13 +60,14 @@ func resourceGithubRepository() *schema.Resource { "security_and_analysis": { Type: schema.TypeList, Optional: true, + Computed: true, MaxItems: 1, Description: "Security and analysis settings for the repository. To use this parameter you must have admin permissions for the repository or be an owner or security manager for the organization that owns the repository.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "advanced_security": { Type: schema.TypeList, - Required: true, + Optional: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -80,7 +81,7 @@ func resourceGithubRepository() *schema.Resource { }, "secret_scanning": { Type: schema.TypeList, - Required: true, + Optional: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -94,7 +95,7 @@ func resourceGithubRepository() *schema.Resource { }, "secret_scanning_push_protection": { Type: schema.TypeList, - Required: true, + Optional: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -352,6 +353,54 @@ func calculateVisibility(d *schema.ResourceData) string { return "public" } +func tryGetSecurityAndAnalysisSettingStatus(securityAndAnalysis map[string]interface{}, setting string) (bool, string) { + value, ok := securityAndAnalysis[setting] + if !ok { + return false, "" + } + + asList := value.([]interface{}) + if len(asList) == 0 || asList[0] == nil { + return false, "" + } + + return true, asList[0].(map[string]interface{})["status"].(string) +} + +func calculateSecurityAndAnalysis(d *schema.ResourceData) *github.SecurityAndAnalysis { + value, ok := d.GetOk("security_and_analysis") + if !ok { + return nil + } + + asList := value.([]interface{}) + if len(asList) == 0 || asList[0] == nil { + return nil + } + + lookup := asList[0].(map[string]interface{}) + + var securityAndAnalysis github.SecurityAndAnalysis + + if ok, status := tryGetSecurityAndAnalysisSettingStatus(lookup, "advanced_security"); ok { + securityAndAnalysis.AdvancedSecurity = &github.AdvancedSecurity{ + Status: github.String(status), + } + } + if ok, status := tryGetSecurityAndAnalysisSettingStatus(lookup, "secret_scanning"); ok { + securityAndAnalysis.SecretScanning = &github.SecretScanning{ + Status: github.String(status), + } + } + if ok, status := tryGetSecurityAndAnalysisSettingStatus(lookup, "secret_scanning_push_protection"); ok { + securityAndAnalysis.SecretScanningPushProtection = &github.SecretScanningPushProtection{ + Status: github.String(status), + } + } + + return &securityAndAnalysis +} + func resourceGithubRepositoryObject(d *schema.ResourceData) *github.Repository { return &github.Repository{ Name: github.String(d.Get("name").(string)), @@ -379,6 +428,7 @@ func resourceGithubRepositoryObject(d *schema.ResourceData) *github.Repository { Archived: github.Bool(d.Get("archived").(bool)), Topics: expandStringList(d.Get("topics").(*schema.Set).List()), AllowUpdateBranch: github.Bool(d.Get("allow_update_branch").(bool)), + SecurityAndAnalysis: calculateSecurityAndAnalysis(d), } } @@ -477,14 +527,6 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta interface{}) er } } - securityAndAnalysis := expandSecurityAndAnalysis(d.Get("security_and_analysis").([]interface{})) - if securityAndAnalysis != nil { - _, _, err := client.Repositories.Edit(ctx, owner, repoName, securityAndAnalysis) - if err != nil { - return err - } - } - return resourceGithubRepositoryUpdate(d, meta) } @@ -634,29 +676,6 @@ func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta interface{}) er } } - if d.HasChange("security_and_analysis") && !d.IsNewResource() { - opts := expandSecurityAndAnalysis(d.Get("security_and_analysis").([]interface{})) - if opts != nil { - _, _, err := client.Repositories.Edit(ctx, owner, repoName, opts) - if err != nil { - return err - } - } else { // disable security and analysis - _, _, err := client.Repositories.Edit(ctx, owner, repoName, &github.Repository{ - SecurityAndAnalysis: &github.SecurityAndAnalysis{ - AdvancedSecurity: &github.AdvancedSecurity{ - Status: github.String("disabled")}, - SecretScanning: &github.SecretScanning{ - Status: github.String("disabled")}, - SecretScanningPushProtection: &github.SecretScanningPushProtection{ - Status: github.String("disabled")}}, - }) - if err != nil { - return err - } - } - } - if d.HasChange("topics") { topics := repoReq.Topics _, _, err = client.Repositories.ReplaceAllTopics(ctx, owner, *repo.Name, topics) @@ -815,45 +834,22 @@ func flattenSecurityAndAnalysis(securityAndAnalysis *github.SecurityAndAnalysis) return []interface{}{} } - advancedSecurityMap := make(map[string]interface{}) - advancedSecurityMap["status"] = securityAndAnalysis.GetAdvancedSecurity().GetStatus() - - secretScanningMap := make(map[string]interface{}) - secretScanningMap["status"] = securityAndAnalysis.GetSecretScanning().GetStatus() - - secretScanningPushProtectionMap := make(map[string]interface{}) - secretScanningPushProtectionMap["status"] = securityAndAnalysis.GetSecretScanningPushProtection().GetStatus() - securityAndAnalysisMap := make(map[string]interface{}) - securityAndAnalysisMap["advanced_security"] = []interface{}{advancedSecurityMap} - securityAndAnalysisMap["secret_scanning"] = []interface{}{secretScanningMap} - securityAndAnalysisMap["secret_scanning_push_protection"] = []interface{}{secretScanningPushProtectionMap} - - return []interface{}{securityAndAnalysisMap} -} - -func expandSecurityAndAnalysis(input []interface{}) *github.Repository { - if len(input) == 0 || input[0] == nil { - return nil - } - - securityAndAnalysis := input[0].(map[string]interface{}) - update := &github.SecurityAndAnalysis{} - advancedSecurity := securityAndAnalysis["advanced_security"].([]interface{})[0].(map[string]interface{}) - update.AdvancedSecurity = &github.AdvancedSecurity{ - Status: github.String(advancedSecurity["status"].(string)), + advancedSecurity := securityAndAnalysis.GetAdvancedSecurity() + if advancedSecurity != nil { + securityAndAnalysisMap["advanced_security"] = []interface{}{map[string]interface{}{ + "status": advancedSecurity.GetStatus(), + }} } - secretScanning := securityAndAnalysis["secret_scanning"].([]interface{})[0].(map[string]interface{}) - update.SecretScanning = &github.SecretScanning{ - Status: github.String(secretScanning["status"].(string)), - } + securityAndAnalysisMap["secret_scanning"] = []interface{}{map[string]interface{}{ + "status": securityAndAnalysis.GetSecretScanning().GetStatus(), + }} - secretScanningPushProtection := securityAndAnalysis["secret_scanning_push_protection"].([]interface{})[0].(map[string]interface{}) - update.SecretScanningPushProtection = &github.SecretScanningPushProtection{ - Status: github.String(secretScanningPushProtection["status"].(string)), - } + securityAndAnalysisMap["secret_scanning_push_protection"] = []interface{}{map[string]interface{}{ + "status": securityAndAnalysis.GetSecretScanningPushProtection().GetStatus(), + }} - return &github.Repository{SecurityAndAnalysis: update} + return []interface{}{securityAndAnalysisMap} } diff --git a/github/resource_github_repository_test.go b/github/resource_github_repository_test.go index 37f37c3dc4..106e1c776e 100644 --- a/github/resource_github_repository_test.go +++ b/github/resource_github_repository_test.go @@ -3,12 +3,13 @@ package github import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "log" "regexp" "strings" "testing" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" ) @@ -820,57 +821,120 @@ func TestAccGithubRepositorySecurity(t *testing.T) { t.Run("manages the security feature for a repository", func(t *testing.T) { - config := fmt.Sprintf(` - resource "github_repository" "test" { - name = "tf-acc-%s" - description = "A repository created by Terraform to test security features" - visibility = "internal" - security_and_analysis { - advanced_security { - status = "enabled" - } - secret_scanning { - status = "enabled" + t.Run("for a private repository", func(t *testing.T) { + t.Skip("organization/individual must have purchased Advanced Security in order to enable it") + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-%s" + description = "A repository created by Terraform to test security features" + visibility = "private" + security_and_analysis { + advanced_security { + status = "enabled" + } + secret_scanning { + status = "enabled" + } + secret_scanning_push_protection { + status = "enabled" + } } - secret_scanning_push_protection { - status = "enabled" } - } - `, randomID) + `, randomID) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_repository.test", "security_and_analysis.0.advanced_security.0.status", - "enabled", - ), - resource.TestCheckResourceAttr( - "github_repository.test", "security_and_analysis.0.secret_scanning.0.status", - "enabled", - ), - ) - testCase := func(t *testing.T, mode string) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnlessMode(t, mode) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: config, - Check: check, + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository.test", "security_and_analysis.0.advanced_security.0.status", + "enabled", + ), + resource.TestCheckResourceAttr( + "github_repository.test", "security_and_analysis.0.secret_scanning.0.status", + "enabled", + ), + resource.TestCheckResourceAttr( + "github_repository.test", "security_and_analysis.0.secret_scanning_push_protection.0.status", + "disabled", + ), + ) + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, }, - }, + }) + } + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") }) - } - t.Run("with an anonymous account", func(t *testing.T) { - t.Skip("anonymous account not supported for this operation") - }) + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) - t.Run("with an individual account", func(t *testing.T) { - t.Skip("individual account not supported for this operation") + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) }) - t.Run("with an organization account", func(t *testing.T) { - testCase(t, organization) + t.Run("for a public repository", func(t *testing.T) { + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-%s" + description = "A repository created by Terraform to test security features" + visibility = "public" + security_and_analysis { + secret_scanning { + status = "enabled" + } + # seems like it can only be "enabled" for an organization that has purchased GHAS + secret_scanning_push_protection { + status = "disabled" + } + } + } + `, randomID) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository.test", "security_and_analysis.0.secret_scanning.0.status", + "enabled", + ), + resource.TestCheckResourceAttr( + "github_repository.test", "security_and_analysis.0.secret_scanning_push_protection.0.status", + "disabled", + ), + ) + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) }) }) } diff --git a/website/docs/r/repository.html.markdown b/website/docs/r/repository.html.markdown index 957a0dc5eb..cc82731b46 100644 --- a/website/docs/r/repository.html.markdown +++ b/website/docs/r/repository.html.markdown @@ -140,7 +140,7 @@ The `source` block supports the following: The `security_and_analysis` block supports the following: -* `advanced_security` - (Required) The advanced security configuration for the repository. See [Advanced Security Configuration](#advanced-security-configuration) below for details. +* `advanced_security` - (Optional) The advanced security configuration for the repository. See [Advanced Security Configuration](#advanced-security-configuration) below for details. If a repository's visibility is `public`, advanced security is always enabled and cannot be changed, so this setting cannot be supplied. * `secret_scanning` - (Required) The secret scanning configuration for the repository. See [Secret Scanning Configuration](#secret-scanning-configuration) below for details. @@ -154,11 +154,11 @@ The `advanced_security` block supports the following: #### Secret Scanning Configuration #### -* `status` - (Required) Set to `enabled` to enable secret scanning on the repository. Can be `enabled` or `disabled`. +* `status` - (Required) Set to `enabled` to enable secret scanning on the repository. Can be `enabled` or `disabled`. If set to `enabled`, the repository's visibility must be `public` or `security_and_analysis[0].advanced_security[0].status` must also be set to `enabled`. #### Secret Scanning Push Protection Configuration #### -* `status` - (Required) Set to `enabled` to enable secret scanning push protection on the repository. Can be `enabled` or `disabled`. +* `status` - (Required) Set to `enabled` to enable secret scanning push protection on the repository. Can be `enabled` or `disabled`. If set to `enabled`, the repository's visibility must be `public` or `security_and_analysis[0].advanced_security[0].status` must also be set to `enabled`. ### Template Repositories