diff --git a/github/provider.go b/github/provider.go index 55cf6f1cd8..aa0f95f79e 100644 --- a/github/provider.go +++ b/github/provider.go @@ -78,6 +78,7 @@ func Provider() terraform.ResourceProvider { "github_actions_environment_secret": resourceGithubActionsEnvironmentSecret(), "github_actions_organization_secret": resourceGithubActionsOrganizationSecret(), "github_actions_organization_secret_repositories": resourceGithubActionsOrganizationSecretRepositories(), + "github_actions_organization_permissions": resourceGithubActionsOrganizationPermissions(), "github_actions_runner_group": resourceGithubActionsRunnerGroup(), "github_actions_secret": resourceGithubActionsSecret(), "github_app_installation_repository": resourceGithubAppInstallationRepository(), diff --git a/github/resource_github_actions_organization_permissions.go b/github/resource_github_actions_organization_permissions.go new file mode 100644 index 0000000000..0eab5e002f --- /dev/null +++ b/github/resource_github_actions_organization_permissions.go @@ -0,0 +1,278 @@ +package github + +import ( + "context" + "errors" + "log" + + "github.com/google/go-github/v39/github" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func resourceGithubActionsOrganizationPermissions() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubActionsOrganizationPermissionsCreateOrUpdate, + Read: resourceGithubActionsOrganizationPermissionsRead, + Update: resourceGithubActionsOrganizationPermissionsCreateOrUpdate, + Delete: resourceGithubActionsOrganizationPermissionsDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "allowed_actions": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"all", "local_only", "selected"}, false), + }, + "enabled_repositories": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"all", "none", "selected"}, false), + }, + "allowed_actions_config": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "github_owned_allowed": { + Type: schema.TypeBool, + Required: true, + }, + "patterns_allowed": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "verified_allowed": { + Type: schema.TypeBool, + Optional: true, + }, + }, + }, + }, + "enabled_repositories_config": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "repository_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeInt}, + Required: true, + }, + }, + }, + }, + }, + } +} + +func resourceGithubActionsOrganizationAllowedObject(d *schema.ResourceData) (*github.ActionsAllowed, error) { + allowed := &github.ActionsAllowed{} + + config := d.Get("allowed_actions_config").([]interface{}) + if len(config) > 0 { + data := config[0].(map[string]interface{}) + switch x := data["github_owned_allowed"].(type) { + case bool: + allowed.GithubOwnedAllowed = &x + } + + switch x := data["verified_allowed"].(type) { + case bool: + allowed.VerifiedAllowed = &x + } + + patternsAllowed := []string{} + + switch t := data["patterns_allowed"].(type) { + case *schema.Set: + for _, value := range t.List() { + patternsAllowed = append(patternsAllowed, value.(string)) + } + } + + allowed.PatternsAllowed = patternsAllowed + } else { + return &github.ActionsAllowed{}, + errors.New("The allowed_actions_config {} block must be specified if allowed_actions == 'selected'.") + } + + return allowed, nil +} + +func resourceGithubActionsEnabledRepositoriesObject(d *schema.ResourceData) ([]int64, error) { + var enabled []int64 + + config := d.Get("enabled_repositories_config").([]interface{}) + log.Printf("[help] length of config in actopms enabled is %v", len(config)) + if len(config) > 0 { + data := config[0].(map[string]interface{}) + switch x := data["repository_ids"].(type) { + case *schema.Set: + for _, value := range x.List() { + enabled = append(enabled, int64(value.(int))) + } + } + } else { + return nil, errors.New("The enabled_repositories_config {} block must be specified if enabled_repositories == 'selected'.") + } + return enabled, nil +} + +func resourceGithubActionsOrganizationPermissionsCreateOrUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + ctx := context.Background() + if !d.IsNewResource() { + ctx = context.WithValue(ctx, ctxId, d.Id()) + } + + err := checkOrganization(meta) + if err != nil { + return err + } + + allowedActions := d.Get("allowed_actions").(string) + enabledRepositories := d.Get("enabled_repositories").(string) + + _, _, err = client.Organizations.EditActionsPermissions(ctx, + orgName, + github.ActionsPermissions{ + AllowedActions: &allowedActions, + EnabledRepositories: &enabledRepositories, + }) + if err != nil { + return err + } + + if allowedActions == "selected" { + actionsAllowedData, err := resourceGithubActionsOrganizationAllowedObject(d) + if err != nil { + return err + } + _, _, err = client.Organizations.EditActionsAllowed(ctx, + orgName, + *actionsAllowedData) + if err != nil { + return err + } + } + + if enabledRepositories == "selected" { + enabledReposData, err := resourceGithubActionsEnabledRepositoriesObject(d) + if err != nil { + return err + } + _, err = client.Actions.SetEnabledReposInOrg(ctx, + orgName, + enabledReposData) + if err != nil { + return err + } + } + + d.SetId(orgName) + return resourceGithubActionsOrganizationPermissionsRead(d, meta) +} + +func resourceGithubActionsOrganizationPermissionsRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + ctx := context.Background() + + err := checkOrganization(meta) + if err != nil { + return err + } + + actionsPermissions, _, err := client.Organizations.GetActionsPermissions(ctx, d.Id()) + if err != nil { + return err + } + + if actionsPermissions.GetAllowedActions() == "selected" { + actionsAllowed, _, err := client.Organizations.GetActionsAllowed(ctx, d.Id()) + if err != nil { + return err + } + + // If actionsAllowed set to local/all by removing all actions config settings, the response will be empty + if actionsAllowed != nil { + d.Set("allowed_actions_config", []interface{}{ + map[string]interface{}{ + "github_owned_allowed": actionsAllowed.GetGithubOwnedAllowed(), + "patterns_allowed": actionsAllowed.PatternsAllowed, + "verified_allowed": actionsAllowed.GetVerifiedAllowed(), + }, + }) + } + } else { + d.Set("allowed_actions_config", []interface{}{}) + } + + if actionsPermissions.GetEnabledRepositories() == "selected" { + opts := github.ListOptions{PerPage: 10, Page: 1} + var repoList []int64 + var allRepos []*github.Repository + + for { + enabledRepos, resp, err := client.Actions.ListEnabledReposInOrg(ctx, d.Id(), &opts) + if err != nil { + return err + } + allRepos = append(allRepos, enabledRepos.Repositories...) + + opts.Page = resp.NextPage + + if resp.NextPage == 0 { + break + } + } + for index := range allRepos { + repoList = append(repoList, *allRepos[index].ID) + } + if allRepos != nil { + d.Set("enabled_repositories_config", []interface{}{ + map[string]interface{}{ + "repository_ids": repoList, + }, + }) + } else { + d.Set("enabled_repositories_config", []interface{}{}) + } + } + + d.Set("allowed_actions", actionsPermissions.GetAllowedActions()) + d.Set("enabled_repositories", actionsPermissions.GetEnabledRepositories()) + + return nil +} + +func resourceGithubActionsOrganizationPermissionsDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + err := checkOrganization(meta) + if err != nil { + return err + } + + // This will nullify any allowedActions elements + _, _, err = client.Organizations.EditActionsPermissions(ctx, + orgName, + github.ActionsPermissions{ + AllowedActions: github.String("all"), + EnabledRepositories: github.String("all"), + }) + if err != nil { + return err + } + + return nil +} diff --git a/github/resource_github_actions_organization_permissions_test.go b/github/resource_github_actions_organization_permissions_test.go new file mode 100644 index 0000000000..0b9effba0c --- /dev/null +++ b/github/resource_github_actions_organization_permissions_test.go @@ -0,0 +1,230 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccGithubActionsOrganizationPermissions(t *testing.T) { + + t.Run("test setting of basic actions organization permissions", func(t *testing.T) { + + allowedActions := "local_only" + enabledRepositories := "all" + + config := fmt.Sprintf(` + resource "github_actions_organization_permissions" "test" { + allowed_actions = "%s" + enabled_repositories = "%s" + } + `, allowedActions, enabledRepositories) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_actions_organization_permissions.test", "allowed_actions", allowedActions, + ), + resource.TestCheckResourceAttr( + "github_actions_organization_permissions.test", "enabled_repositories", enabledRepositories, + ), + ) + + 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 organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) + + t.Run("imports entire set of github action organization permissions without error", func(t *testing.T) { + + allowedActions := "selected" + enabledRepositories := "selected" + githubOwnedAllowed := true + verifiedAllowed := true + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-test-topic-%[1]s" + description = "Terraform acceptance tests %[1]s" + topics = ["terraform", "testing"] + } + + resource "github_actions_organization_permissions" "test" { + allowed_actions = "%s" + enabled_repositories = "%s" + allowed_actions_config { + github_owned_allowed = %t + patterns_allowed = ["actions/cache@*", "actions/checkout@*"] + verified_allowed = %t + } + enabled_repositories_config { + repository_ids = [github_repository.test.repo_id] + } + } + `, randomID, allowedActions, enabledRepositories, githubOwnedAllowed, verifiedAllowed) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_actions_organization_permissions.test", "allowed_actions", allowedActions, + ), + resource.TestCheckResourceAttr( + "github_actions_organization_permissions.test", "enabled_repositories", enabledRepositories, + ), + resource.TestCheckResourceAttr( + "github_actions_organization_permissions.test", "allowed_actions_config.#", "1", + ), + resource.TestCheckResourceAttr( + "github_actions_organization_permissions.test", "enabled_repositories_config.#", "1", + ), + ) + + 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, + }, + { + ResourceName: "github_actions_organization_permissions.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + } + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) + + t.Run("test setting of organization allowed actions", func(t *testing.T) { + + allowedActions := "selected" + enabledRepositories := "all" + githubOwnedAllowed := true + verifiedAllowed := true + + config := fmt.Sprintf(` + + resource "github_actions_organization_permissions" "test" { + allowed_actions = "%s" + enabled_repositories = "%s" + allowed_actions_config { + github_owned_allowed = %t + patterns_allowed = ["actions/cache@*", "actions/checkout@*"] + verified_allowed = %t + } + } + `, allowedActions, enabledRepositories, githubOwnedAllowed, verifiedAllowed) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_actions_organization_permissions.test", "allowed_actions", allowedActions, + ), + resource.TestCheckResourceAttr( + "github_actions_organization_permissions.test", "enabled_repositories", enabledRepositories, + ), + resource.TestCheckResourceAttr( + "github_actions_organization_permissions.test", "allowed_actions_config.#", "1", + ), + ) + + 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 organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) + + t.Run("test setting of organization enabled repositories", func(t *testing.T) { + + allowedActions := "all" + enabledRepositories := "selected" + githubOwnedAllowed := true + verifiedAllowed := true + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + randomID2 := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-test-topic-%[1]s" + description = "Terraform acceptance tests %[1]s" + topics = ["terraform", "testing"] + } + + resource "github_repository" "test2" { + name = "tf-acc-test-topic-%[2]s" + description = "Terraform acceptance tests %[2]s" + topics = ["terraform", "testing"] + } + + resource "github_actions_organization_permissions" "test" { + allowed_actions = "%s" + enabled_repositories = "%s" + enabled_repositories_config { + repository_ids = [github_repository.test.repo_id, github_repository.test2.repo_id] + } + } + `, randomID, randomID2, allowedActions, enabledRepositories, githubOwnedAllowed, verifiedAllowed) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_actions_organization_permissions.test", "allowed_actions", allowedActions, + ), + resource.TestCheckResourceAttr( + "github_actions_organization_permissions.test", "enabled_repositories", enabledRepositories, + ), + resource.TestCheckResourceAttr( + "github_actions_organization_permissions.test", "enabled_repositories_config.#", "1", + ), + ) + + 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 organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) + +} diff --git a/website/docs/r/actions_organization_permissions.html.markdown b/website/docs/r/actions_organization_permissions.html.markdown new file mode 100644 index 0000000000..e3a386028c --- /dev/null +++ b/website/docs/r/actions_organization_permissions.html.markdown @@ -0,0 +1,63 @@ +--- +layout: "github" +page_title: "GitHub: github_actions_organization_permissions" +description: |- + Creates and manages Actions permissions within a GitHub organization +--- + +# github_actions_organization_permissions + +This resource allows you to create and manage GitHub Actions permissions within your GitHub enterprise organizations. +You must have admin access to an organization to use this resource. + +## Example Usage + +```hcl +resource "github_repository" "example" { + name = "my-repository" +} + +resource "github_actions_organization_permissions" "test" { + allowed_actions = "selected" + enabled_repositories = "selected" + allowed_actions_config { + github_owned_allowed = true + patterns_allowed = ["actions/cache@*", "actions/checkout@*"] + verified_allowed = true + } + enabled_repositories_config { + repository_ids = [github_repository.example.repo_id] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `allowed_actions` - (Optional) The permissions policy that controls the actions that are allowed to run. Can be one of: `all`, `local_only`, or `selected`. +* `enabled_repositories` - (Required) The policy that controls the repositories in the organization that are allowed to run GitHub Actions. Can be one of: `all`, `none`, or `selected`. +* `allowed_actions_config` - (Optional) Sets the actions that are allowed in an organization. Only available when `allowed_actions` = `selected`. See [Allowed Actions Config](#allowed-actions-config) below for details. +* `enabled_repositories_config` - (Optional) Sets the list of selected repositories that are enabled for GitHub Actions in an organization. Only available when `enabled_repositories` = `selected`. See [Enabled Repositories Config](#enabled-repositories-config) below for details. + +### Allowed Actions Config + +The `allowed_actions_config` block supports the following: + +* `github_owned_allowed` - (Required) Whether GitHub-owned actions are allowed in the organization. +* `patterns_allowed` - (Optional) Specifies a list of string-matching patterns to allow specific action(s). Wildcards, tags, and SHAs are allowed. For example, monalisa/octocat@*, monalisa/octocat@v2, monalisa/*." +* `verified_allowed` - (Optional) Whether actions in GitHub Marketplace from verified creators are allowed. Set to true to allow all GitHub Marketplace actions by verified creators. + +### Enabled Repositories Config + +The `enabled_repositories_config` block supports the following: + +* `repository_ids` - (Required) List of repository IDs to enable for GitHub Actions. + +## Import + +This resource can be imported using the ID of the GitHub organization: + +``` +$ terraform import github_actions_organization_permissions.test +``` diff --git a/website/github.erb b/website/github.erb index 1d900f1000..4d974aa4e8 100644 --- a/website/github.erb +++ b/website/github.erb @@ -67,6 +67,9 @@
  • github_actions_organization_secret
  • +
  • + github_actions_organization_permissions +
  • github_actions_organization_secret_repositories