Skip to content

Commit

Permalink
Add support for organization security managers (integrations#1371)
Browse files Browse the repository at this point in the history
* add org security managers resource

* log error on too many security managers

* add default state import by team id

* add security manager resource docs

* Add missing error check

Co-authored-by: Keegan Campbell <me@kfcampbell.com>
  • Loading branch information
jeremyhayes and kfcampbell authored Nov 28, 2022
1 parent 21d6609 commit 0b48264
Show file tree
Hide file tree
Showing 5 changed files with 362 additions and 0 deletions.
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func Provider() terraform.ResourceProvider {
"github_membership": resourceGithubMembership(),
"github_organization_block": resourceOrganizationBlock(),
"github_organization_project": resourceGithubOrganizationProject(),
"github_organization_security_manager": resourceGithubOrganizationSecurityManager(),
"github_organization_settings": resourceGithubOrganizationSettings(),
"github_organization_webhook": resourceGithubOrganizationWebhook(),
"github_project_card": resourceGithubProjectCard(),
Expand Down
151 changes: 151 additions & 0 deletions github/resource_github_organization_security_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package github

import (
"context"
"log"
"net/http"
"strconv"

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

func resourceGithubOrganizationSecurityManager() *schema.Resource {
return &schema.Resource{
Create: resourceGithubOrganizationSecurityManagerCreate,
Read: resourceGithubOrganizationSecurityManagerRead,
Update: resourceGithubOrganizationSecurityManagerUpdate,
Delete: resourceGithubOrganizationSecurityManagerDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"team_slug": {
Type: schema.TypeString,
Required: true,
},
},
}
}

func resourceGithubOrganizationSecurityManagerCreate(d *schema.ResourceData, meta interface{}) error {
err := checkOrganization(meta)
if err != nil {
return err
}

orgName := meta.(*Owner).name
teamSlug := d.Get("team_slug").(string)

client := meta.(*Owner).v3client
ctx := context.Background()

team, _, err := client.Teams.GetTeamBySlug(ctx, orgName, teamSlug)
if err != nil {
log.Printf("[INFO] Team %s/%s was not found in GitHub", orgName, teamSlug)
return err
}

_, err = client.Organizations.AddSecurityManagerTeam(ctx, orgName, teamSlug)
if err != nil {
if ghErr, ok := err.(*github.ErrorResponse); ok {
if ghErr.Response.StatusCode == http.StatusConflict {
log.Printf("[WARN] Organization %s has reached the maximum number of security manager teams", orgName)
return nil
}
}
return err
}

d.SetId(strconv.FormatInt(team.GetID(), 10))

return resourceGithubOrganizationSecurityManagerRead(d, meta)
}

func resourceGithubOrganizationSecurityManagerRead(d *schema.ResourceData, meta interface{}) error {
err := checkOrganization(meta)
if err != nil {
return err
}

orgName := meta.(*Owner).name
teamId, err := strconv.ParseInt(d.Id(), 10, 64)
if err != nil {
return err
}

client := meta.(*Owner).v3client
ctx := context.WithValue(context.Background(), ctxId, d.Id())

// There is no endpoint for getting a single security manager team, so get the list and filter.
// There is a maximum number of security manager teams (currently 10), so this should be fine.
teams, _, err := client.Organizations.ListSecurityManagerTeams(ctx, orgName)
if err != nil {
return err
}

var team *github.Team
for _, t := range teams {
if t.GetID() == teamId {
team = t
break
}
}

if team == nil {
log.Printf("[WARN] Removing organization security manager team %s from state because it no longer exists in GitHub", d.Id())
d.SetId("")
return nil
}

d.Set("team_slug", team.GetSlug())

return nil
}

func resourceGithubOrganizationSecurityManagerUpdate(d *schema.ResourceData, meta interface{}) error {
err := checkOrganization(meta)
if err != nil {
return err
}

orgId := meta.(*Owner).id
orgName := meta.(*Owner).name
teamId, err := strconv.ParseInt(d.Id(), 10, 64)
if err != nil {
return err
}

client := meta.(*Owner).v3client
ctx := context.WithValue(context.Background(), ctxId, d.Id())

team, _, err := client.Teams.GetTeamByID(ctx, orgId, teamId)
if err != nil {
return err
}

// Adding the same team is a no-op.
_, err = client.Organizations.AddSecurityManagerTeam(ctx, orgName, team.GetSlug())
if err != nil {
return err
}

return resourceGithubOrganizationSecurityManagerRead(d, meta)
}

func resourceGithubOrganizationSecurityManagerDelete(d *schema.ResourceData, meta interface{}) error {
err := checkOrganization(meta)
if err != nil {
return err
}

orgName := meta.(*Owner).name
teamSlug := d.Get("team_slug").(string)

client := meta.(*Owner).v3client
ctx := context.WithValue(context.Background(), ctxId, d.Id())

_, err = client.Organizations.RemoveSecurityManagerTeam(ctx, orgName, teamSlug)
return err
}
172 changes: 172 additions & 0 deletions github/resource_github_organization_security_manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package github

import (
"fmt"
"testing"

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

func TestAccGithubOrganizationSecurityManagers(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

t.Run("adds team as security manager", func(t *testing.T) {
config := fmt.Sprintf(`
resource "github_team" "test" {
name = "tf-acc-%s"
}
resource "github_organization_security_manager" "test" {
team_slug = github_team.test.slug
}
`, randomID)

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: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrPair("github_team.test", "ID", "github_organization_security_manager.test", "ID"),
resource.TestCheckResourceAttrPair("github_team.test", "slug", "github_organization_security_manager.test", "team_slug"),
resource.TestCheckResourceAttr("github_organization_security_manager.test", "team_slug", fmt.Sprintf("tf-acc-%s", randomID)),
),
},
},
})
}

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

t.Run("handles team name changes", func(t *testing.T) {
config := fmt.Sprintf(`
resource "github_team" "test" {
name = "tf-acc-%s"
}
resource "github_organization_security_manager" "test" {
team_slug = github_team.test.slug
}
`, randomID)

configUpdated := fmt.Sprintf(`
resource "github_team" "test" {
name = "tf-acc-updated-%s"
}
resource "github_organization_security_manager" "test" {
team_slug = github_team.test.slug
}
`, randomID)

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: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrPair("github_team.test", "ID", "github_organization_security_manager.test", "ID"),
resource.TestCheckResourceAttrPair("github_team.test", "slug", "github_organization_security_manager.test", "team_slug"),
resource.TestCheckResourceAttr("github_organization_security_manager.test", "team_slug", fmt.Sprintf("tf-acc-%s", randomID)),
),
},
{
Config: configUpdated,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrPair("github_team.test", "ID", "github_organization_security_manager.test", "ID"),
resource.TestCheckResourceAttrPair("github_team.test", "slug", "github_organization_security_manager.test", "team_slug"),
resource.TestCheckResourceAttr("github_organization_security_manager.test", "team_slug", fmt.Sprintf("tf-acc-updated-%s", randomID)),
),
},
},
})
}

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

t.Run("handles team name changes", func(t *testing.T) {
config := fmt.Sprintf(`
resource "github_team" "test" {
name = "tf-acc-%s"
}
resource "github_organization_security_manager" "test" {
team_slug = github_team.test.slug
}
`, randomID)

configUpdated := fmt.Sprintf(`
resource "github_team" "test" {
name = "tf-acc-updated-%s"
}
resource "github_organization_security_manager" "test" {
team_slug = github_team.test.slug
}
`, randomID)

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: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrPair("github_team.test", "ID", "github_organization_security_manager.test", "ID"),
resource.TestCheckResourceAttrPair("github_team.test", "slug", "github_organization_security_manager.test", "team_slug"),
resource.TestCheckResourceAttr("github_organization_security_manager.test", "team_slug", fmt.Sprintf("tf-acc-%s", randomID)),
),
},
{
Config: configUpdated,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrPair("github_team.test", "ID", "github_organization_security_manager.test", "ID"),
resource.TestCheckResourceAttrPair("github_team.test", "slug", "github_organization_security_manager.test", "team_slug"),
resource.TestCheckResourceAttr("github_organization_security_manager.test", "team_slug", fmt.Sprintf("tf-acc-updated-%s", randomID)),
),
},
},
})
}

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)
})
})
}
35 changes: 35 additions & 0 deletions website/docs/r/organization_security_manager.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
layout: "github"
page_title: "GitHub: github_organization_security_manager"
description: |-
Manages the Security manager teams for a GitHub Organization.
---

# github_organization_security_manager

## Example Usage

```hcl
resource "github_team" "some_team" {
name = "SomeTeam"
description = "Some cool team"
}
resource "github_organization_security_manager" "some_team" {
team_slug = github_team.some_team.slug
}
```

## Argument Reference

The following arguments are supported:

* `team_slug` - (Required) The slug of the team to manage.

## Import

GitHub Security Manager Teams can be imported using the GitHub team ID e.g.

```
$ terraform import github_organization_security_manager.core 1234567
```
3 changes: 3 additions & 0 deletions website/github.erb
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@
<li>
<a href="/docs/providers/github/r/organization_project.html">github_organization_project</a>
</li>
<li>
<a href="/docs/providers/github/r/organization_security_manager.html">github_organization_security_manager</a>
</li>
<li>
<a href="/docs/providers/github/r/organization_settings.html">github_organization_settings</a>
</li>
Expand Down

0 comments on commit 0b48264

Please sign in to comment.