Skip to content

Commit

Permalink
feat(branch_protection): Add support for force push bypassers (#1529)
Browse files Browse the repository at this point in the history
* feat(branch_protection): Add support for force push bypassers

Fixes #1085.

* Fixes from @pkrzaczkowski-hippo

#1529 (comment)
  • Loading branch information
reedloden authored Jun 15, 2023
1 parent c5c8a15 commit 42c2a60
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 6 deletions.
38 changes: 32 additions & 6 deletions github/resource_github_branch_protection.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ func resourceGithubBranchProtection() *schema.Resource {
Description: "The list of actor Names/IDs that may push to the branch. Actor names must either begin with a '/' for users or the organization name followed by a '/' for teams.",
Elem: &schema.Schema{Type: schema.TypeString},
},
PROTECTION_FORCE_PUSHES_BYPASSERS: {
Type: schema.TypeSet,
Optional: true,
Description: "The list of actor Names/IDs that are allowed to bypass force push restrictions. Actor names must either begin with a '/' for users or the organization name followed by a '/' for teams.",
Elem: &schema.Schema{Type: schema.TypeString},
},
},

Create: resourceGithubBranchProtectionCreate,
Expand Down Expand Up @@ -185,7 +191,7 @@ func resourceGithubBranchProtectionCreate(d *schema.ResourceData, meta interface
return err
}

var reviewIds, pushIds, bypassIds []string
var reviewIds, pushIds, bypassForcePushIds, bypassPullRequestIds []string
reviewIds, err = getActorIds(data.ReviewDismissalActorIDs, meta)
if err != nil {
return err
Expand All @@ -196,19 +202,26 @@ func resourceGithubBranchProtectionCreate(d *schema.ResourceData, meta interface
return err
}

bypassIds, err = getActorIds(data.BypassPullRequestActorIDs, meta)
bypassForcePushIds, err = getActorIds(data.BypassForcePushActorIDs, meta)
if err != nil {
return err
}

bypassPullRequestIds, err = getActorIds(data.BypassPullRequestActorIDs, meta)
if err != nil {
return err
}

data.PushActorIDs = pushIds
data.ReviewDismissalActorIDs = reviewIds
data.BypassPullRequestActorIDs = bypassIds
data.BypassForcePushActorIDs = bypassForcePushIds
data.BypassPullRequestActorIDs = bypassPullRequestIds

input := githubv4.CreateBranchProtectionRuleInput{
AllowsDeletions: githubv4.NewBoolean(githubv4.Boolean(data.AllowsDeletions)),
AllowsForcePushes: githubv4.NewBoolean(githubv4.Boolean(data.AllowsForcePushes)),
BlocksCreations: githubv4.NewBoolean(githubv4.Boolean(data.BlocksCreations)),
BypassForcePushActorIDs: githubv4NewIDSlice(githubv4IDSliceEmpty(data.BypassForcePushActorIDs)),
BypassPullRequestActorIDs: githubv4NewIDSlice(githubv4IDSliceEmpty(data.BypassPullRequestActorIDs)),
DismissesStaleReviews: githubv4.NewBoolean(githubv4.Boolean(data.DismissesStaleReviews)),
IsAdminEnforced: githubv4.NewBoolean(githubv4.Boolean(data.IsAdminEnforced)),
Expand Down Expand Up @@ -331,6 +344,12 @@ func resourceGithubBranchProtectionRead(d *schema.ResourceData, meta interface{}
log.Printf("[DEBUG] Problem setting '%s' in %s %s branch protection (%s)", PROTECTION_RESTRICTS_PUSHES, protection.Repository.Name, protection.Pattern, d.Id())
}

forcePushBypassers := setForcePushBypassers(protection, data, meta)
err = d.Set(PROTECTION_FORCE_PUSHES_BYPASSERS, forcePushBypassers)
if err != nil {
log.Printf("[DEBUG] Problem setting '%s' in %s %s branch protection (%s)", PROTECTION_FORCE_PUSHES_BYPASSERS, protection.Repository.Name, protection.Pattern, d.Id())
}

err = d.Set(PROTECTION_REQUIRES_LAST_PUSH_APPROVAL, protection.RequireLastPushApproval)
if err != nil {
log.Printf("[DEBUG] Problem setting '%s' in %s %s branch protection (%s)", PROTECTION_REQUIRES_LAST_PUSH_APPROVAL, protection.Repository.Name, protection.Pattern, d.Id())
Expand All @@ -357,7 +376,7 @@ func resourceGithubBranchProtectionUpdate(d *schema.ResourceData, meta interface
return err
}

var reviewIds, pushIds, bypassIds []string
var reviewIds, pushIds, bypassForcePushIds, bypassPullRequestIds []string
reviewIds, err = getActorIds(data.ReviewDismissalActorIDs, meta)
if err != nil {
return err
Expand All @@ -368,20 +387,27 @@ func resourceGithubBranchProtectionUpdate(d *schema.ResourceData, meta interface
return err
}

bypassIds, err = getActorIds(data.BypassPullRequestActorIDs, meta)
bypassForcePushIds, err = getActorIds(data.BypassForcePushActorIDs, meta)
if err != nil {
return err
}

bypassPullRequestIds, err = getActorIds(data.BypassPullRequestActorIDs, meta)
if err != nil {
return err
}

data.PushActorIDs = pushIds
data.ReviewDismissalActorIDs = reviewIds
data.BypassPullRequestActorIDs = bypassIds
data.BypassForcePushActorIDs = bypassForcePushIds
data.BypassPullRequestActorIDs = bypassPullRequestIds

input := githubv4.UpdateBranchProtectionRuleInput{
BranchProtectionRuleID: d.Id(),
AllowsDeletions: githubv4.NewBoolean(githubv4.Boolean(data.AllowsDeletions)),
AllowsForcePushes: githubv4.NewBoolean(githubv4.Boolean(data.AllowsForcePushes)),
BlocksCreations: githubv4.NewBoolean(githubv4.Boolean(data.BlocksCreations)),
BypassForcePushActorIDs: githubv4NewIDSlice(githubv4IDSliceEmpty(data.BypassForcePushActorIDs)),
BypassPullRequestActorIDs: githubv4NewIDSlice(githubv4IDSliceEmpty(data.BypassPullRequestActorIDs)),
DismissesStaleReviews: githubv4.NewBoolean(githubv4.Boolean(data.DismissesStaleReviews)),
IsAdminEnforced: githubv4.NewBoolean(githubv4.Boolean(data.IsAdminEnforced)),
Expand Down
112 changes: 112 additions & 0 deletions github/resource_github_branch_protection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,118 @@ func TestAccGithubBranchProtection(t *testing.T) {

})

t.Run("configures non-empty list of force push bypassers", func(t *testing.T) {

config := fmt.Sprintf(`
resource "github_repository" "test" {
name = "tf-acc-test-%s"
auto_init = true
}
data "github_user" "test" {
username = "%s"
}
resource "github_branch_protection" "test" {
repository_id = github_repository.test.node_id
pattern = "main"
force_push_bypassers = [
data.github_user.test.node_id
]
}
`, randomID, testOwnerFunc())

check := resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(
"github_branch_protection.test", "force_push_bypassers.#", "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 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) {
testCase(t, individual)
})

t.Run("with an organization account", func(t *testing.T) {
testCase(t, organization)
})

})

t.Run("configures empty list of force push bypassers", func(t *testing.T) {

config := fmt.Sprintf(`
resource "github_repository" "test" {
name = "tf-acc-test-%s"
auto_init = true
}
resource "github_branch_protection" "test" {
repository_id = github_repository.test.node_id
pattern = "main"
force_push_bypassers = []
}
`, randomID)

check := resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(
"github_branch_protection.test", "force_push_bypassers.#", "0",
),
)

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) {
testCase(t, individual)
})

t.Run("with an organization account", func(t *testing.T) {
testCase(t, organization)
})

})

t.Run("configures non-empty list of pull request bypassers", func(t *testing.T) {

config := fmt.Sprintf(`
Expand Down
78 changes: 78 additions & 0 deletions github/util_v4_branch_protection.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ type DismissalActorTypes struct {
}
}

type BypassForcePushActorTypes struct {
Actor struct {
App Actor `graphql:"... on App"`
Team Actor `graphql:"... on Team"`
User ActorUser `graphql:"... on User"`
}
}

type BypassPullRequestActorTypes struct {
Actor struct {
App Actor `graphql:"... on App"`
Expand Down Expand Up @@ -56,6 +64,9 @@ type BranchProtectionRule struct {
ReviewDismissalAllowances struct {
Nodes []DismissalActorTypes
} `graphql:"reviewDismissalAllowances(first: 100)"`
BypassForcePushAllowances struct {
Nodes []BypassForcePushActorTypes
} `graphql:"bypassForcePushAllowances(first: 100)"`
BypassPullRequestAllowances struct {
Nodes []BypassPullRequestActorTypes
} `graphql:"bypassPullRequestAllowances(first: 100)"`
Expand Down Expand Up @@ -86,6 +97,7 @@ type BranchProtectionResourceData struct {
AllowsForcePushes bool
BlocksCreations bool
BranchProtectionRuleID string
BypassForcePushActorIDs []string
BypassPullRequestActorIDs []string
DismissesStaleReviews bool
IsAdminEnforced bool
Expand Down Expand Up @@ -237,6 +249,18 @@ func branchProtectionResourceData(d *schema.ResourceData, meta interface{}) (Bra
}
}

if v, ok := d.GetOk(PROTECTION_FORCE_PUSHES_BYPASSERS); ok {
bypassForcePushActorIDs := make([]string, 0)
vL := v.(*schema.Set).List()
for _, v := range vL {
bypassForcePushActorIDs = append(bypassForcePushActorIDs, v.(string))
}
if len(bypassForcePushActorIDs) > 0 {
data.BypassForcePushActorIDs = bypassForcePushActorIDs
data.AllowsForcePushes = false
}
}

if v, ok := d.GetOk(PROTECTION_LOCK_BRANCH); ok {
data.LockBranch = v.(bool)
}
Expand Down Expand Up @@ -293,6 +317,19 @@ func branchProtectionResourceDataActors(d *schema.ResourceData, meta interface{}
data.RestrictsPushes = true
}
}

if v, ok := d.GetOk(PROTECTION_FORCE_PUSHES_BYPASSERS); ok {
bypassForcePushActorIDs := make([]string, 0)
vL := v.(*schema.Set).List()
for _, v := range vL {
bypassForcePushActorIDs = append(bypassForcePushActorIDs, v.(string))
}
if len(bypassForcePushActorIDs) > 0 {
data.BypassForcePushActorIDs = bypassForcePushActorIDs
data.AllowsForcePushes = false
}
}

return data, nil
}

Expand Down Expand Up @@ -322,6 +359,37 @@ func setDismissalActorIDs(actors []DismissalActorTypes, data BranchProtectionRes
return dismissalActors
}

func setBypassForcePushActorIDs(actors []BypassForcePushActorTypes, data BranchProtectionResourceData, meta interface{}) []string {
bypassActors := make([]string, 0, len(actors))

orgName := meta.(*Owner).name

for _, a := range actors {
IsID := false
for _, v := range data.BypassForcePushActorIDs {
if (a.Actor.Team.ID != nil && a.Actor.Team.ID.(string) == v) || (a.Actor.User.ID != nil && a.Actor.User.ID.(string) == v) || (a.Actor.App.ID != nil && a.Actor.App.ID.(string) == v) {
bypassActors = append(bypassActors, v)
IsID = true
break
}
}
if !IsID {
if a.Actor.Team.Slug != "" {
bypassActors = append(bypassActors, orgName+"/"+string(a.Actor.Team.Slug))
continue
}
if a.Actor.User.Login != "" {
bypassActors = append(bypassActors, "/"+string(a.Actor.User.Login))
continue
}
if a.Actor.App != (Actor{}) {
bypassActors = append(bypassActors, a.Actor.App.ID.(string))
}
}
}
return bypassActors
}

func setBypassPullRequestActorIDs(actors []BypassPullRequestActorTypes, data BranchProtectionResourceData, meta interface{}) []string {
bypassActors := make([]string, 0, len(actors))

Expand Down Expand Up @@ -434,6 +502,16 @@ func setPushes(protection BranchProtectionRule, data BranchProtectionResourceDat
return pushActors
}

func setForcePushBypassers(protection BranchProtectionRule, data BranchProtectionResourceData, meta interface{}) []string {
if protection.AllowsForcePushes {
return nil
}
bypassForcePushAllowances := protection.BypassForcePushAllowances.Nodes
bypassForcePushActors := setBypassForcePushActorIDs(bypassForcePushAllowances, data, meta)

return bypassForcePushActors
}

func getBranchProtectionID(repoID githubv4.ID, pattern string, meta interface{}) (githubv4.ID, error) {
var query struct {
Node struct {
Expand Down
1 change: 1 addition & 0 deletions github/util_v4_consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
PROTECTION_RESTRICTS_PUSHES = "push_restrictions"
PROTECTION_RESTRICTS_REVIEW_DISMISSALS = "restrict_dismissals"
PROTECTION_RESTRICTS_REVIEW_DISMISSERS = "dismissal_restrictions"
PROTECTION_FORCE_PUSHES_BYPASSERS = "force_push_bypassers"
PROTECTION_PULL_REQUESTS_BYPASSERS = "pull_request_bypassers"
PROTECTION_LOCK_BRANCH = "lock_branch"
PROTECTION_REQUIRES_LAST_PUSH_APPROVAL = "require_last_push_approval"
Expand Down
9 changes: 9 additions & 0 deletions website/docs/r/branch_protection.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ resource "github_branch_protection" "example" {
# github_team.example.node_id
]
force_push_bypassers = [
data.github_user.example.node_id,
"/exampleuser",
"exampleorganization/exampleteam",
# limited to a list of one type of restriction (user, team, app)
# github_team.example.node_id
]
}
resource "github_repository" "example" {
Expand Down Expand Up @@ -85,6 +93,7 @@ The following arguments are supported:
* `required_status_checks` - (Optional) Enforce restrictions for required status checks. See [Required Status Checks](#required-status-checks) below for details.
* `required_pull_request_reviews` - (Optional) Enforce restrictions for pull request reviews. See [Required Pull Request Reviews](#required-pull-request-reviews) below for details.
* `push_restrictions` - (Optional) The list of actor Names/IDs that may push to the branch. Actor names must either begin with a "/" for users or the organization name followed by a "/" for teams.
* `force_push_bypassers` - (Optional) The list of actor Names/IDs that are allowed to bypass force push restrictions. Actor names must either begin with a "/" for users or the organization name followed by a "/" for teams.
* `allows_deletions` - (Optional) Boolean, setting this to `true` to allow the branch to be deleted.
* `allows_force_pushes` - (Optional) Boolean, setting this to `true` to allow force pushes on the branch.
* `blocks_creations` - (Optional) Boolean, setting this to `true` to block creating the branch.
Expand Down

0 comments on commit 42c2a60

Please sign in to comment.