diff --git a/github/provider.go b/github/provider.go index cf044eee66..b843f503a9 100644 --- a/github/provider.go +++ b/github/provider.go @@ -219,6 +219,7 @@ func Provider() *schema.Provider { "github_enterprise_ip_allow_list_entry": resourceGithubEnterpriseIpAllowListEntry(), "github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(), "github_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(), + "github_enterprise_private_repository_forking_setting": resourceGithubEnterprisePrivateRepositoryForkingSetting(), "github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(), "github_workflow_repository_permissions": resourceGithubWorkflowRepositoryPermissions(), }, diff --git a/github/resource_github_enterprise_private_repository_forking_setting.go b/github/resource_github_enterprise_private_repository_forking_setting.go new file mode 100644 index 0000000000..3be3427c57 --- /dev/null +++ b/github/resource_github_enterprise_private_repository_forking_setting.go @@ -0,0 +1,192 @@ +package github + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/shurcooL/githubv4" +) + +func resourceGithubEnterprisePrivateRepositoryForkingSetting() *schema.Resource { + return &schema.Resource{ + Description: "Manages the private repository forking policy for a GitHub Enterprise.", + Create: resourceGithubEnterprisePrivateRepositoryForkingSettingCreateOrUpdate, + Read: resourceGithubEnterprisePrivateRepositoryForkingSettingRead, + Update: resourceGithubEnterprisePrivateRepositoryForkingSettingCreateOrUpdate, + Delete: resourceGithubEnterprisePrivateRepositoryForkingSettingDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + CustomizeDiff: func(_ context.Context, diff *schema.ResourceDiff, _ any) error { + settingValue := diff.Get("setting").(string) + policyValue := diff.Get("policy").(string) + + if settingValue == "ENABLED" && policyValue == "" { + return fmt.Errorf("policy is required when setting is ENABLED") + } + if settingValue != "ENABLED" && policyValue != "" { + return fmt.Errorf("policy must not be set when setting is %s", settingValue) + } + return nil + }, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The slug of the enterprise.", + }, + "setting": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "ENABLED", + "DISABLED", + "NO_POLICY", + }, false)), + Description: "Whether private repository forking is enabled for the enterprise. Must be one of: ENABLED, DISABLED, NO_POLICY.", + }, + "policy": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "ENTERPRISE_ORGANIZATIONS", + "SAME_ORGANIZATION", + "SAME_ORGANIZATION_USER_ACCOUNTS", + "ENTERPRISE_ORGANIZATIONS_USER_ACCOUNTS", + "USER_ACCOUNTS", + "EVERYWHERE", + }, false)), + Description: "Where members can fork private repositories. Required when setting is ENABLED. Must be one of: ENTERPRISE_ORGANIZATIONS, SAME_ORGANIZATION, SAME_ORGANIZATION_USER_ACCOUNTS, ENTERPRISE_ORGANIZATIONS_USER_ACCOUNTS, USER_ACCOUNTS, EVERYWHERE.", + }, + }, + } +} + +func resourceGithubEnterprisePrivateRepositoryForkingSettingCreateOrUpdate(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v4client + ctx := context.Background() + + enterpriseSlug := d.Get("enterprise_slug").(string) + + enterpriseID, err := getEnterpriseID(ctx, client, enterpriseSlug) + if err != nil { + return fmt.Errorf("error resolving enterprise ID for slug %q: %w", enterpriseSlug, err) + } + + settingValue := githubv4.EnterpriseEnabledDisabledSettingValue(d.Get("setting").(string)) + + input := githubv4.UpdateEnterpriseAllowPrivateRepositoryForkingSettingInput{ + EnterpriseID: enterpriseID, + SettingValue: settingValue, + } + + if v, ok := d.GetOk("policy"); ok { + pv := githubv4.EnterpriseAllowPrivateRepositoryForkingPolicyValue(v.(string)) + input.PolicyValue = &pv + } + + var mutate struct { + UpdateEnterpriseAllowPrivateRepositoryForkingSetting struct { + Enterprise struct { + ID githubv4.ID + } + Message githubv4.String + } `graphql:"updateEnterpriseAllowPrivateRepositoryForkingSetting(input: $input)"` + } + + log.Printf("[DEBUG] Updating private repository forking setting for enterprise: %s", enterpriseSlug) + err = client.Mutate(ctx, &mutate, input, nil) + if err != nil { + return fmt.Errorf("error updating private repository forking setting for enterprise %q: %w", enterpriseSlug, err) + } + + d.SetId(enterpriseSlug) + + return resourceGithubEnterprisePrivateRepositoryForkingSettingRead(d, meta) +} + +func resourceGithubEnterprisePrivateRepositoryForkingSettingRead(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v4client + ctx := context.Background() + + enterpriseSlug := d.Id() + + var query struct { + Enterprise struct { + OwnerInfo struct { + AllowPrivateRepositoryForkingSetting githubv4.EnterpriseEnabledDisabledSettingValue + AllowPrivateRepositoryForkingSettingPolicyValue githubv4.EnterpriseAllowPrivateRepositoryForkingPolicyValue + } + } `graphql:"enterprise(slug: $slug)"` + } + + variables := map[string]any{ + "slug": githubv4.String(enterpriseSlug), + } + + log.Printf("[DEBUG] Reading private repository forking setting for enterprise: %s", enterpriseSlug) + err := client.Query(ctx, &query, variables) + if err != nil { + return fmt.Errorf("error reading private repository forking setting for enterprise %q: %w", enterpriseSlug, err) + } + + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return err + } + + settingValue := string(query.Enterprise.OwnerInfo.AllowPrivateRepositoryForkingSetting) + if err := d.Set("setting", settingValue); err != nil { + return err + } + + if settingValue == "ENABLED" { + if err := d.Set("policy", string(query.Enterprise.OwnerInfo.AllowPrivateRepositoryForkingSettingPolicyValue)); err != nil { + return err + } + } else { + if err := d.Set("policy", ""); err != nil { + return err + } + } + + return nil +} + +func resourceGithubEnterprisePrivateRepositoryForkingSettingDelete(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v4client + ctx := context.Background() + + enterpriseSlug := d.Id() + + enterpriseID, err := getEnterpriseID(ctx, client, enterpriseSlug) + if err != nil { + return fmt.Errorf("error resolving enterprise ID for slug %q: %w", enterpriseSlug, err) + } + + input := githubv4.UpdateEnterpriseAllowPrivateRepositoryForkingSettingInput{ + EnterpriseID: enterpriseID, + SettingValue: githubv4.EnterpriseEnabledDisabledSettingValueNoPolicy, + } + + var mutate struct { + UpdateEnterpriseAllowPrivateRepositoryForkingSetting struct { + Enterprise struct { + ID githubv4.ID + } + } `graphql:"updateEnterpriseAllowPrivateRepositoryForkingSetting(input: $input)"` + } + + log.Printf("[DEBUG] Resetting private repository forking setting to NO_POLICY for enterprise: %s", enterpriseSlug) + err = client.Mutate(ctx, &mutate, input, nil) + if err != nil { + return fmt.Errorf("error resetting private repository forking setting for enterprise %q: %w", enterpriseSlug, err) + } + + return nil +} diff --git a/github/resource_github_enterprise_private_repository_forking_setting_test.go b/github/resource_github_enterprise_private_repository_forking_setting_test.go new file mode 100644 index 0000000000..d9d68ad8e3 --- /dev/null +++ b/github/resource_github_enterprise_private_repository_forking_setting_test.go @@ -0,0 +1,156 @@ +package github + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testAccEnterpriseForkingSettingResource = "github_enterprise_private_repository_forking_setting.test" + +func testAccEnterpriseForkingSettingConfig(settingValue, policyValue string) string { + if policyValue != "" { + return fmt.Sprintf(` + resource "github_enterprise_private_repository_forking_setting" "test" { + enterprise_slug = "%s" + setting = "%s" + policy = "%s" + } + `, testAccConf.enterpriseSlug, settingValue, policyValue) + } + return fmt.Sprintf(` + resource "github_enterprise_private_repository_forking_setting" "test" { + enterprise_slug = "%s" + setting = "%s" + } + `, testAccConf.enterpriseSlug, settingValue) +} + +func testAccEnterpriseForkingSettingCheck(settingValue, policyValue string) resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(testAccEnterpriseForkingSettingResource, "enterprise_slug", testAccConf.enterpriseSlug), + resource.TestCheckResourceAttr(testAccEnterpriseForkingSettingResource, "setting", settingValue), + resource.TestCheckResourceAttr(testAccEnterpriseForkingSettingResource, "policy", policyValue), + ) +} + +func TestAccGithubEnterprisePrivateRepositoryForkingSetting(t *testing.T) { + t.Run("enables private repository forking with policy", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccEnterpriseForkingSettingConfig("ENABLED", "SAME_ORGANIZATION"), + Check: testAccEnterpriseForkingSettingCheck("ENABLED", "SAME_ORGANIZATION"), + }, + }, + }) + }) + + t.Run("updates policy value", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccEnterpriseForkingSettingConfig("ENABLED", "SAME_ORGANIZATION"), + Check: testAccEnterpriseForkingSettingCheck("ENABLED", "SAME_ORGANIZATION"), + }, + { + Config: testAccEnterpriseForkingSettingConfig("ENABLED", "ENTERPRISE_ORGANIZATIONS_USER_ACCOUNTS"), + Check: testAccEnterpriseForkingSettingCheck("ENABLED", "ENTERPRISE_ORGANIZATIONS_USER_ACCOUNTS"), + }, + }, + }) + }) + + t.Run("disables private repository forking", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccEnterpriseForkingSettingConfig("DISABLED", ""), + Check: testAccEnterpriseForkingSettingCheck("DISABLED", ""), + }, + }, + }) + }) + + t.Run("sets no policy", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccEnterpriseForkingSettingConfig("NO_POLICY", ""), + Check: testAccEnterpriseForkingSettingCheck("NO_POLICY", ""), + }, + }, + }) + }) + + t.Run("transitions from enabled to disabled", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccEnterpriseForkingSettingConfig("ENABLED", "SAME_ORGANIZATION"), + Check: testAccEnterpriseForkingSettingCheck("ENABLED", "SAME_ORGANIZATION"), + }, + { + Config: testAccEnterpriseForkingSettingConfig("DISABLED", ""), + Check: testAccEnterpriseForkingSettingCheck("DISABLED", ""), + }, + }, + }) + }) + + t.Run("rejects policy when disabled", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccEnterpriseForkingSettingConfig("DISABLED", "SAME_ORGANIZATION"), + ExpectError: regexp.MustCompile(`policy must not be set when setting is DISABLED`), + }, + }, + }) + }) + + t.Run("requires policy when enabled", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccEnterpriseForkingSettingConfig("ENABLED", ""), + ExpectError: regexp.MustCompile(`policy is required when setting is ENABLED`), + }, + }, + }) + }) + + t.Run("imports without error", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccEnterpriseForkingSettingConfig("ENABLED", "ENTERPRISE_ORGANIZATIONS_USER_ACCOUNTS"), + Check: testAccEnterpriseForkingSettingCheck("ENABLED", "ENTERPRISE_ORGANIZATIONS_USER_ACCOUNTS"), + }, + { + ResourceName: testAccEnterpriseForkingSettingResource, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) +} diff --git a/website/docs/r/enterprise_private_repository_forking_setting.html.markdown b/website/docs/r/enterprise_private_repository_forking_setting.html.markdown new file mode 100644 index 0000000000..3b79631417 --- /dev/null +++ b/website/docs/r/enterprise_private_repository_forking_setting.html.markdown @@ -0,0 +1,84 @@ +--- +layout: "github" +page_title: "GitHub: github_enterprise_private_repository_forking_setting" +description: |- + Creates and manages the private repository forking policy for a GitHub Enterprise. +--- + +# github_enterprise_private_repository_forking_setting + +This resource allows you to create and manage the private repository forking policy for a GitHub Enterprise. +You must have enterprise admin access to use this resource. + +When `setting` is `ENABLED`, the `policy` attribute controls where forks +can be created. When `DISABLED`, forking of private repositories is not allowed. +When `NO_POLICY`, individual organizations within the enterprise control their own +forking settings. + +## Example Usage + +### Restrict forking to same organization only + +```hcl +resource "github_enterprise_private_repository_forking_setting" "example" { + enterprise_slug = "my-enterprise" + setting = "ENABLED" + policy = "SAME_ORGANIZATION" +} +``` + +### Allow forking to enterprise-managed user accounts or enterprise organizations + +```hcl +resource "github_enterprise_private_repository_forking_setting" "example" { + enterprise_slug = "my-enterprise" + setting = "ENABLED" + policy = "ENTERPRISE_ORGANIZATIONS_USER_ACCOUNTS" +} +``` + +### Disable private repository forking entirely + +```hcl +resource "github_enterprise_private_repository_forking_setting" "example" { + enterprise_slug = "my-enterprise" + setting = "DISABLED" +} +``` + +### Allow organizations to set their own policy + +```hcl +resource "github_enterprise_private_repository_forking_setting" "example" { + enterprise_slug = "my-enterprise" + setting = "NO_POLICY" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required) The slug of the enterprise. +* `setting` - (Required) Whether private repository forking is enabled for the enterprise. Must be one of `ENABLED`, `DISABLED`, or `NO_POLICY`. +* `policy` - (Optional) Where members can fork private repositories. Required when `setting` is `ENABLED`. Must be one of: + * `ENTERPRISE_ORGANIZATIONS` - Members can fork to an organization within this enterprise. + * `SAME_ORGANIZATION` - Members can fork only within the same organization (intra-org). + * `SAME_ORGANIZATION_USER_ACCOUNTS` - Members can fork to their user account or within the same organization. + * `ENTERPRISE_ORGANIZATIONS_USER_ACCOUNTS` - Members can fork to their enterprise-managed user account or an organization inside this enterprise. + * `USER_ACCOUNTS` - Members can fork to their user account. + * `EVERYWHERE` - Members can fork to their user account or an organization, either inside or outside of this enterprise. + +**Note:** Destroying this resource sets the enterprise policy to `NO_POLICY`, which allows individual organizations to control their own forking settings. It does not set the policy to `DISABLED`. + +## Attributes Reference + +No additional attributes are exported. + +## Import + +Enterprise private repository forking settings can be imported using the enterprise slug: + +``` +$ terraform import github_enterprise_private_repository_forking_setting.example my-enterprise +```