diff --git a/github/provider.go b/github/provider.go index 943e4a2d4f..2291d401a4 100644 --- a/github/provider.go +++ b/github/provider.go @@ -162,6 +162,7 @@ func Provider() *schema.Provider { "github_organization_custom_properties": resourceGithubOrganizationCustomProperties(), "github_organization_project": resourceGithubOrganizationProject(), "github_organization_security_manager": resourceGithubOrganizationSecurityManager(), + "github_organization_role_team_assignment": resourceGithubOrganizationRoleTeamAssignment(), "github_organization_ruleset": resourceGithubOrganizationRuleset(), "github_organization_settings": resourceGithubOrganizationSettings(), "github_organization_webhook": resourceGithubOrganizationWebhook(), diff --git a/github/resource_github_organization_role_team_assignment.go b/github/resource_github_organization_role_team_assignment.go new file mode 100644 index 0000000000..c0e5167754 --- /dev/null +++ b/github/resource_github_organization_role_team_assignment.go @@ -0,0 +1,153 @@ +package github + +import ( + "context" + "log" + "strconv" + + "github.com/google/go-github/v66/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubOrganizationRoleTeamAssignment() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubOrganizationRoleTeamAssignmentCreate, + Read: resourceGithubOrganizationRoleTeamAssignmentRead, + Delete: resourceGithubOrganizationRoleTeamAssignmentDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "team_slug": { + Type: schema.TypeString, + Required: true, + Description: "The GitHub team slug.", + ForceNew: true, + }, + "role_id": { + Type: schema.TypeString, + Required: true, + Description: "The GitHub organization role id", + ForceNew: true, + }, + }, + } +} + +func resourceGithubOrganizationRoleTeamAssignmentCreate(d *schema.ResourceData, meta interface{}) error { + err := checkOrganization(meta) + if err != nil { + return err + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + ctx := context.Background() + + teamSlug := d.Get("team_slug").(string) + roleIDString := d.Get("role_id").(string) + + roleID, err := strconv.ParseInt(roleIDString, 10, 64) + if err != nil { + return err + } + + _, err = client.Organizations.AssignOrgRoleToTeam(ctx, orgName, teamSlug, roleID) + if err != nil { + return err + } + + d.SetId(buildTwoPartID(teamSlug, roleIDString)) + return resourceGithubOrganizationRoleTeamAssignmentRead(d, meta) +} + +func resourceGithubOrganizationRoleTeamAssignmentRead(d *schema.ResourceData, meta interface{}) error { + err := checkOrganization(meta) + if err != nil { + return err + } + + client := meta.(*Owner).v3client + ctx := context.Background() + orgName := meta.(*Owner).name + + teamSlug, roleIDString, err := parseTwoPartID(d.Id(), "team_slug", "role_id") + if err != nil { + return err + } + + roleID, err := strconv.ParseInt(roleIDString, 10, 64) + if err != nil { + return err + } + + // There is no api for checking a specific team role assignment, so instead we iterate over all teams assigned to the role + // go-github pagination (https://github.com/google/go-github?tab=readme-ov-file#pagination) + options := &github.ListOptions{ + PerPage: 100, + } + var foundTeam *github.Team + for { + teams, resp, err := client.Organizations.ListTeamsAssignedToOrgRole(ctx, orgName, roleID, options) + if err != nil { + return err + } + + for _, team := range teams { + if team.GetSlug() == teamSlug { + foundTeam = team + break + } + + } + + if resp.NextPage == 0 { + break + } + options.Page = resp.NextPage + } + + if foundTeam == nil { + log.Printf("[WARN] Removing team organization role association %s from state because it no longer exists in GitHub", d.Id()) + d.SetId("") + return nil + } + + if err = d.Set("team_slug", teamSlug); err != nil { + return err + } + if err = d.Set("role_id", roleIDString); err != nil { + return err + } + + return nil +} + +func resourceGithubOrganizationRoleTeamAssignmentDelete(d *schema.ResourceData, meta interface{}) error { + err := checkOrganization(meta) + if err != nil { + return err + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + ctx := context.Background() + + teamSlug, roleIDString, err := parseTwoPartID(d.Id(), "team_slug", "role_id") + if err != nil { + return err + } + + roleID, err := strconv.ParseInt(roleIDString, 10, 64) + if err != nil { + return err + } + + _, err = client.Organizations.RemoveOrgRoleFromTeam(ctx, orgName, teamSlug, roleID) + if err != nil { + return err + } + + return nil +} diff --git a/github/resource_github_organization_role_team_assignment_test.go b/github/resource_github_organization_role_team_assignment_test.go new file mode 100644 index 0000000000..a705563aec --- /dev/null +++ b/github/resource_github_organization_role_team_assignment_test.go @@ -0,0 +1,144 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccGithubOrganizationRoleTeamAssignment(t *testing.T) { + + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + // Using the predefined roles since custom roles are a strictly Enterprise feature ((https://github.blog/changelog/2024-07-10-pre-defined-organization-roles-that-grant-access-to-all-repositories/)) + githubPredefinedRoleMapping := make(map[string]string) + githubPredefinedRoleMapping["all_repo_read"] = "8132" + githubPredefinedRoleMapping["all_repo_triage"] = "8133" + githubPredefinedRoleMapping["all_repo_write"] = "8134" + githubPredefinedRoleMapping["all_repo_maintain"] = "8135" + githubPredefinedRoleMapping["all_repo_admin"] = "8136" + + t.Run("creates repo assignment without error", func(t *testing.T) { + + teamSlug := fmt.Sprintf("tf-acc-test-team-repo-%s", randomID) + config := fmt.Sprintf(` + resource "github_team" "test" { + name = "%s" + description = "test" + } + resource github_organization_role_team_assignment "test" { + team_slug = github_team.test.slug + role_id = "%s" + } + `, teamSlug, githubPredefinedRoleMapping["all_repo_read"]) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_organization_role_team_assignment.test", "id", fmt.Sprintf("%s:%s", teamSlug, githubPredefinedRoleMapping["all_repo_read"]), + ), + resource.TestCheckResourceAttr( + "github_organization_role_team_assignment.test", "team_slug", teamSlug, + ), + resource.TestCheckResourceAttr( + "github_organization_role_team_assignment.test", "role_id", githubPredefinedRoleMapping["all_repo_read"], + ), + ) + + 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) { + t.Skip("individual account not supported for this operation") + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) + + // More tests can go here following the same format... + t.Run("create and re-creates role assignment without error", func(t *testing.T) { + + teamSlug := fmt.Sprintf("tf-acc-test-team-repo-%s", randomID) + configs := map[string]string{ + "before": fmt.Sprintf(` + resource "github_team" "test" { + name = "%s" + description = "test" + } + resource github_organization_role_team_assignment "test" { + team_slug = github_team.test.slug + role_id = "%s" + } + `, teamSlug, githubPredefinedRoleMapping["all_repo_read"]), + "after": fmt.Sprintf(` + resource "github_team" "test" { + name = "%s" + description = "test" + } + resource github_organization_role_team_assignment "test" { + team_slug = github_team.test.slug + role_id = "%s" + } + `, teamSlug, githubPredefinedRoleMapping["all_repo_write"]), + } + + checks := map[string]resource.TestCheckFunc{ + "before": resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_organization_role_team_assignment.test", "role_id", githubPredefinedRoleMapping["all_repo_read"], + ), + ), + "after": resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_organization_role_team_assignment.test", "role_id", githubPredefinedRoleMapping["all_repo_write"], + ), + ), + } + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: configs["before"], + Check: checks["before"], + }, + { + Config: configs["after"], + Check: checks["after"], + }, + }, + }) + } + + 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) { + t.Skip("individual account not supported for this operation") + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) +} diff --git a/website/docs/r/organization_role_team_assignment.html.markdown b/website/docs/r/organization_role_team_assignment.html.markdown new file mode 100644 index 0000000000..ef5373b1c5 --- /dev/null +++ b/website/docs/r/organization_role_team_assignment.html.markdown @@ -0,0 +1,44 @@ +--- +layout: "github" +page_title: "GitHub: github_organization_role_team_assignment" +description: |- + Manages the associations between teams and organization roles. +--- + +# github_organization_role_team_assignment + +This resource manages relationships between teams and organization roles +in your GitHub organization. This works on predefined roles, and custom roles, where the latter is an Enterprise feature. + +Creating this resource assigns the role to a team. + +The organization role and team must both belong to the same organization +on GitHub. + +## Example Usage + +```hcl +resource "github_team" "test-team" { + name = "test-team" +} + +resource "github_organization_role_team_assignment" "test-team-role-assignment" { + team_slug = github_team.test-team.slug + role_id = "8132" # all_repo_read (predefined) +} +``` + +## Argument Reference + +The following arguments are supported: + +* `team_slug` - (Required) The GitHub team slug +* `role_id` - (Required) The GitHub organization role id + +## Import + +GitHub Team Organization Role Assignment can be imported using an ID made up of `team_slug:role_id` + +``` +$ terraform import github_organization_role_team_assignment.role_assignment test-team:8132 +```