Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support team organization role assignment #2322

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ func Provider() *schema.Provider {
"github_team": resourceGithubTeam(),
"github_team_members": resourceGithubTeamMembers(),
"github_team_membership": resourceGithubTeamMembership(),
"github_team_organization_role_assignment": resourceGithubTeamOrganizationRoleAssignment(),
"github_team_repository": resourceGithubTeamRepository(),
"github_team_settings": resourceGithubTeamSettings(),
"github_team_sync_group_mapping": resourceGithubTeamSyncGroupMapping(),
Expand Down
181 changes: 181 additions & 0 deletions github/resource_github_team_organization_role_assignment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package github

import (
"context"
"log"
"strconv"

"github.com/google/go-github/v66/github"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceGithubTeamOrganizationRoleAssignment() *schema.Resource {
return &schema.Resource{
Create: resourceGithubTeamOrganizationRoleAssignmentCreate,
Read: resourceGithubTeamOrganizationRoleAssignmentRead,
Delete: resourceGithubTeamOrganizationRoleAssignmentDelete,
Importer: &schema.ResourceImporter{
State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
teamIdString, roleID, err := parseTwoPartID(d.Id(), "team_id", "role_id")
if err != nil {
return nil, err
}

teamSlug, err := getTeamSlug(teamIdString, meta)
if err != nil {
return nil, err
}

d.SetId(buildTwoPartID(teamSlug, roleID))
return []*schema.ResourceData{d}, nil
},
},

Schema: map[string]*schema.Schema{
"team_id": {
Type: schema.TypeString,
Required: true,
Description: "The GitHub team id or the GitHub team slug.",
ForceNew: true,
},
"role_id": {
Type: schema.TypeString,
Required: true,
Description: "The GitHub Organization Role id.",
ForceNew: true,
},
},
}
}

func resourceGithubTeamOrganizationRoleAssignmentCreate(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()

// The given team id could be an id or a slug
givenTeamId := d.Get("team_id").(string)
teamSlug, err := getTeamSlug(givenTeamId, meta)
if err != nil {
return err
}

roleIDString := d.Get("role_id").(string)
roleID, err := strconv.ParseInt(roleIDString, 10, 32)

if err != nil {
return err
}

_, err = client.Organizations.AssignOrgRoleToTeam(ctx, orgName, teamSlug, roleID)
if err != nil {
return err
}

d.SetId(buildTwoPartID(teamSlug, roleIDString))
return resourceGithubTeamOrganizationRoleAssignmentRead(d, meta)
}

func resourceGithubTeamOrganizationRoleAssignmentRead(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

teamIdString, roleIDString, err := parseTwoPartID(d.Id(), "team_id", "role_id")
if err != nil {
return err
}

// The given team id could be an id or a slug
teamSlug, err := getTeamSlug(teamIdString, meta)
if err != nil {
return err
}

roleID, err := strconv.ParseInt(roleIDString, 10, 32)
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using google/go-github here instead of octokit/go-sdk since the former doesn't support 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
}

return nil
}

func resourceGithubTeamOrganizationRoleAssignmentDelete(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()

teamIdString, roleIDString, err := parseTwoPartID(d.Id(), "team_id", "role_id")
if err != nil {
return err
}

// The given team id could be an id or a slug
teamSlug, err := getTeamSlug(teamIdString, meta)
if err != nil {
return err
}

roleID, err := strconv.ParseInt(roleIDString, 10, 32)
if err != nil {
return err
}

if err != nil {
return err
}

_, err = client.Organizations.RemoveOrgRoleFromTeam(ctx, orgName, teamSlug, roleID)
if err != nil {
return err
}

return nil
}
142 changes: 142 additions & 0 deletions github/resource_github_team_organization_role_assignment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package github

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccGithubTeamOrganizationRoleAssignment(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/))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since custom organization roles are a Enterprise level feature, I have elected to not use them in the tests and instead use the built-in roles instead. I've created a manual mapping for their role_id:s here. Not ideal, but I don't have a better way of testing this

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) {

config := fmt.Sprintf(`
resource "github_team" "test" {
name = "tf-acc-test-team-repo-%s"
description = "test"
}
resource "github_team_organization_role_assignment" "test" {
team_id = github_team.test.id
role_id = "%s"
}
`, randomID, githubPredefinedRoleMapping["all_repo_read"])

check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(
"github_team_organization_role_assignment.test", "id",
),
resource.TestCheckResourceAttrSet(
"github_team_organization_role_assignment.test", "team_id",
),
resource.TestCheckResourceAttr(
"github_team_organization_role_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) {

configs := map[string]string{
"before": fmt.Sprintf(`
resource "github_team" "test" {
name = "tf-acc-test-team-repo-%s"
description = "test"
}
resource "github_team_organization_role_assignment" "test" {
team_id = github_team.test.id
role_id = "%s"
}
`, randomID, githubPredefinedRoleMapping["all_repo_read"]),
"after": fmt.Sprintf(`
resource "github_team" "test" {
name = "tf-acc-test-team-repo-%s"
description = "test"
}
resource "github_team_organization_role_assignment" "test" {
team_id = github_team.test.id
role_id = "%s"
}
`, randomID, githubPredefinedRoleMapping["all_repo_write"]),
}

checks := map[string]resource.TestCheckFunc{
"before": resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_team_organization_role_assignment.test", "role_id", githubPredefinedRoleMapping["all_repo_read"],
),
),
"after": resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_team_organization_role_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)
})
})
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module github.com/integrations/terraform-provider-github/v6

go 1.21
go 1.21.5

toolchain go1.22.0
toolchain go1.22.3

require (
github.com/client9/misspell v0.3.4
Expand Down
45 changes: 45 additions & 0 deletions website/docs/r/team_organization_role_assignment.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
layout: "github"
page_title: "GitHub: github_team_organization_role_assignment"
description: |-
Manages the associations between teams and organization roles.
---

# github_team_organization_role_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 a 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_team_organization_role_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_id` - (Required) The GitHub team id or 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_id:role_id` or `team_slug:role_id`, e.g.

```
$ terraform import github_team_organization_role_assignment.role_assignment 1234567:8132
$ terraform import github_team_organization_role_assignment.role_assignment test-team:8132
```