Add API branch protection endpoint (#9311)

* add API branch protection endpoint

* lint

* Change to use team names instead of ids.

* Status codes.

* fix

* Fix

* Add new branch protection options (BlockOnRejectedReviews, DismissStaleApprovals, RequireSignedCommits)

* Do xorm query directly

* fix xorm GetUserNamesByIDs

* Add some tests

* Improved GetTeamNamesByID

* http status created for CreateBranchProtection

* Correct status code in integration test

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: zeripath <art27@cantab.net>
This commit is contained in:
David Svantesson
2020-02-13 00:19:35 +01:00
committed by GitHub
parent 908f8952be
commit 9ff4e1d2d9
10 changed files with 1352 additions and 28 deletions

View File

@ -30,6 +30,54 @@ func testAPIGetBranch(t *testing.T, branchName string, exists bool) {
assert.EqualValues(t, branchName, branch.Name)
}
func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session)
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
resp := session.MakeRequest(t, req, expectedHTTPStatus)
if resp.Code == 200 {
var branchProtection api.BranchProtection
DecodeJSON(t, resp, &branchProtection)
assert.EqualValues(t, branchName, branchProtection.BranchName)
}
}
func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session)
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections?token="+token, &api.BranchProtection{
BranchName: branchName,
})
resp := session.MakeRequest(t, req, expectedHTTPStatus)
if resp.Code == 201 {
var branchProtection api.BranchProtection
DecodeJSON(t, resp, &branchProtection)
assert.EqualValues(t, branchName, branchProtection.BranchName)
}
}
func testAPIEditBranchProtection(t *testing.T, branchName string, body *api.BranchProtection, expectedHTTPStatus int) {
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session)
req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user2/repo1/branch_protections/"+branchName+"?token="+token, body)
resp := session.MakeRequest(t, req, expectedHTTPStatus)
if resp.Code == 200 {
var branchProtection api.BranchProtection
DecodeJSON(t, resp, &branchProtection)
assert.EqualValues(t, branchName, branchProtection.BranchName)
}
}
func testAPIDeleteBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session)
req := NewRequestf(t, "DELETE", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
session.MakeRequest(t, req, expectedHTTPStatus)
}
func TestAPIGetBranch(t *testing.T) {
for _, test := range []struct {
BranchName string
@ -43,3 +91,23 @@ func TestAPIGetBranch(t *testing.T) {
testAPIGetBranch(t, test.BranchName, test.Exists)
}
}
func TestAPIBranchProtection(t *testing.T) {
defer prepareTestEnv(t)()
// Branch protection only on branch that exist
testAPICreateBranchProtection(t, "master/doesnotexist", http.StatusNotFound)
// Get branch protection on branch that exist but not branch protection
testAPIGetBranchProtection(t, "master", http.StatusNotFound)
testAPICreateBranchProtection(t, "master", http.StatusCreated)
// Can only create once
testAPICreateBranchProtection(t, "master", http.StatusForbidden)
testAPIGetBranchProtection(t, "master", http.StatusOK)
testAPIEditBranchProtection(t, "master", &api.BranchProtection{
EnablePush: true,
}, http.StatusOK)
testAPIDeleteBranchProtection(t, "master", http.StatusNoContent)
}

View File

@ -553,6 +553,23 @@ func GetTeam(orgID int64, name string) (*Team, error) {
return getTeam(x, orgID, name)
}
// GetTeamIDsByNames returns a slice of team ids corresponds to names.
func GetTeamIDsByNames(orgID int64, names []string, ignoreNonExistent bool) ([]int64, error) {
ids := make([]int64, 0, len(names))
for _, name := range names {
u, err := GetTeam(orgID, name)
if err != nil {
if ignoreNonExistent {
continue
} else {
return nil, err
}
}
ids = append(ids, u.ID)
}
return ids, nil
}
// getOwnerTeam returns team by given team name and organization.
func getOwnerTeam(e Engine, orgID int64) (*Team, error) {
return getTeam(e, orgID, ownerTeamName)
@ -574,6 +591,22 @@ func GetTeamByID(teamID int64) (*Team, error) {
return getTeamByID(x, teamID)
}
// GetTeamNamesByID returns team's lower name from a list of team ids.
func GetTeamNamesByID(teamIDs []int64) ([]string, error) {
if len(teamIDs) == 0 {
return []string{}, nil
}
var teamNames []string
err := x.Table("team").
Select("lower_name").
In("id", teamIDs).
Asc("name").
Find(&teamNames)
return teamNames, err
}
// UpdateTeam updates information of team.
func UpdateTeam(t *Team, authChanged bool, includeAllChanged bool) (err error) {
if len(t.Name) == 0 {

View File

@ -1386,6 +1386,17 @@ func GetMaileableUsersByIDs(ids []int64) ([]*User, error) {
Find(&ous)
}
// GetUserNamesByIDs returns usernames for all resolved users from a list of Ids.
func GetUserNamesByIDs(ids []int64) ([]string, error) {
unames := make([]string, 0, len(ids))
err := x.In("id", ids).
Table("user").
Asc("name").
Cols("name").
Find(&unames)
return unames, err
}
// GetUsersByIDs returns all resolved users from a list of Ids.
func GetUsersByIDs(ids []int64) ([]*User, error) {
ous := make([]*User, 0, len(ids))

View File

@ -30,28 +30,86 @@ func ToEmail(email *models.EmailAddress) *api.Email {
}
// ToBranch convert a git.Commit and git.Branch to an api.Branch
func ToBranch(repo *models.Repository, b *git.Branch, c *git.Commit, bp *models.ProtectedBranch, user *models.User) *api.Branch {
func ToBranch(repo *models.Repository, b *git.Branch, c *git.Commit, bp *models.ProtectedBranch, user *models.User, isRepoAdmin bool) *api.Branch {
if bp == nil {
return &api.Branch{
Name: b.Name,
Commit: ToCommit(repo, c),
Protected: false,
RequiredApprovals: 0,
EnableStatusCheck: false,
StatusCheckContexts: []string{},
UserCanPush: true,
UserCanMerge: true,
Name: b.Name,
Commit: ToCommit(repo, c),
Protected: false,
RequiredApprovals: 0,
EnableStatusCheck: false,
StatusCheckContexts: []string{},
UserCanPush: true,
UserCanMerge: true,
EffectiveBranchProtectionName: "",
}
}
branchProtectionName := ""
if isRepoAdmin {
branchProtectionName = bp.BranchName
}
return &api.Branch{
Name: b.Name,
Commit: ToCommit(repo, c),
Protected: true,
RequiredApprovals: bp.RequiredApprovals,
EnableStatusCheck: bp.EnableStatusCheck,
StatusCheckContexts: bp.StatusCheckContexts,
UserCanPush: bp.CanUserPush(user.ID),
UserCanMerge: bp.IsUserMergeWhitelisted(user.ID),
Name: b.Name,
Commit: ToCommit(repo, c),
Protected: true,
RequiredApprovals: bp.RequiredApprovals,
EnableStatusCheck: bp.EnableStatusCheck,
StatusCheckContexts: bp.StatusCheckContexts,
UserCanPush: bp.CanUserPush(user.ID),
UserCanMerge: bp.IsUserMergeWhitelisted(user.ID),
EffectiveBranchProtectionName: branchProtectionName,
}
}
// ToBranchProtection convert a ProtectedBranch to api.BranchProtection
func ToBranchProtection(bp *models.ProtectedBranch) *api.BranchProtection {
pushWhitelistUsernames, err := models.GetUserNamesByIDs(bp.WhitelistUserIDs)
if err != nil {
log.Error("GetUserNamesByIDs (WhitelistUserIDs): %v", err)
}
mergeWhitelistUsernames, err := models.GetUserNamesByIDs(bp.MergeWhitelistUserIDs)
if err != nil {
log.Error("GetUserNamesByIDs (MergeWhitelistUserIDs): %v", err)
}
approvalsWhitelistUsernames, err := models.GetUserNamesByIDs(bp.ApprovalsWhitelistUserIDs)
if err != nil {
log.Error("GetUserNamesByIDs (ApprovalsWhitelistUserIDs): %v", err)
}
pushWhitelistTeams, err := models.GetTeamNamesByID(bp.WhitelistTeamIDs)
if err != nil {
log.Error("GetTeamNamesByID (WhitelistTeamIDs): %v", err)
}
mergeWhitelistTeams, err := models.GetTeamNamesByID(bp.MergeWhitelistTeamIDs)
if err != nil {
log.Error("GetTeamNamesByID (MergeWhitelistTeamIDs): %v", err)
}
approvalsWhitelistTeams, err := models.GetTeamNamesByID(bp.ApprovalsWhitelistTeamIDs)
if err != nil {
log.Error("GetTeamNamesByID (ApprovalsWhitelistTeamIDs): %v", err)
}
return &api.BranchProtection{
BranchName: bp.BranchName,
EnablePush: bp.CanPush,
EnablePushWhitelist: bp.EnableWhitelist,
PushWhitelistUsernames: pushWhitelistUsernames,
PushWhitelistTeams: pushWhitelistTeams,
PushWhitelistDeployKeys: bp.WhitelistDeployKeys,
EnableMergeWhitelist: bp.EnableMergeWhitelist,
MergeWhitelistUsernames: mergeWhitelistUsernames,
MergeWhitelistTeams: mergeWhitelistTeams,
EnableStatusCheck: bp.EnableStatusCheck,
StatusCheckContexts: bp.StatusCheckContexts,
RequiredApprovals: bp.RequiredApprovals,
EnableApprovalsWhitelist: bp.EnableApprovalsWhitelist,
ApprovalsWhitelistUsernames: approvalsWhitelistUsernames,
ApprovalsWhitelistTeams: approvalsWhitelistTeams,
BlockOnRejectedReviews: bp.BlockOnRejectedReviews,
DismissStaleApprovals: bp.DismissStaleApprovals,
RequireSignedCommits: bp.RequireSignedCommits,
Created: bp.CreatedUnix.AsTime(),
Updated: bp.UpdatedUnix.AsTime(),
}
}

View File

@ -4,14 +4,88 @@
package structs
import (
"time"
)
// Branch represents a repository branch
type Branch struct {
Name string `json:"name"`
Commit *PayloadCommit `json:"commit"`
Protected bool `json:"protected"`
RequiredApprovals int64 `json:"required_approvals"`
EnableStatusCheck bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
UserCanPush bool `json:"user_can_push"`
UserCanMerge bool `json:"user_can_merge"`
Name string `json:"name"`
Commit *PayloadCommit `json:"commit"`
Protected bool `json:"protected"`
RequiredApprovals int64 `json:"required_approvals"`
EnableStatusCheck bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
UserCanPush bool `json:"user_can_push"`
UserCanMerge bool `json:"user_can_merge"`
EffectiveBranchProtectionName string `json:"effective_branch_protection_name"`
}
// BranchProtection represents a branch protection for a repository
type BranchProtection struct {
BranchName string `json:"branch_name"`
EnablePush bool `json:"enable_push"`
EnablePushWhitelist bool `json:"enable_push_whitelist"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
EnableStatusCheck bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
RequiredApprovals int64 `json:"required_approvals"`
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
DismissStaleApprovals bool `json:"dismiss_stale_approvals"`
RequireSignedCommits bool `json:"require_signed_commits"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
}
// CreateBranchProtectionOption options for creating a branch protection
type CreateBranchProtectionOption struct {
BranchName string `json:"branch_name"`
EnablePush bool `json:"enable_push"`
EnablePushWhitelist bool `json:"enable_push_whitelist"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
EnableStatusCheck bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
RequiredApprovals int64 `json:"required_approvals"`
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
DismissStaleApprovals bool `json:"dismiss_stale_approvals"`
RequireSignedCommits bool `json:"require_signed_commits"`
}
// EditBranchProtectionOption options for editing a branch protection
type EditBranchProtectionOption struct {
EnablePush *bool `json:"enable_push"`
EnablePushWhitelist *bool `json:"enable_push_whitelist"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
PushWhitelistDeployKeys *bool `json:"push_whitelist_deploy_keys"`
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
EnableStatusCheck *bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
RequiredApprovals *int64 `json:"required_approvals"`
EnableApprovalsWhitelist *bool `json:"enable_approvals_whitelist"`
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
BlockOnRejectedReviews *bool `json:"block_on_rejected_reviews"`
DismissStaleApprovals *bool `json:"dismiss_stale_approvals"`
RequireSignedCommits *bool `json:"require_signed_commits"`
}

View File

@ -656,6 +656,15 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("", repo.ListBranches)
m.Get("/*", context.RepoRefByType(context.RepoRefBranch), repo.GetBranch)
}, reqRepoReader(models.UnitTypeCode))
m.Group("/branch_protections", func() {
m.Get("", repo.ListBranchProtections)
m.Post("", bind(api.CreateBranchProtectionOption{}), repo.CreateBranchProtection)
m.Group("/:name", func() {
m.Get("", repo.GetBranchProtection)
m.Patch("", bind(api.EditBranchProtectionOption{}), repo.EditBranchProtection)
m.Delete("", repo.DeleteBranchProtection)
})
}, reqToken(), reqAdmin())
m.Group("/tags", func() {
m.Get("", repo.ListTags)
}, reqRepoReader(models.UnitTypeCode), context.ReferencesGitRepo(true))

File diff suppressed because it is too large Load Diff

View File

@ -128,4 +128,10 @@ type swaggerParameterBodies struct {
// in:body
EditReactionOption api.EditReactionOption
// in:body
CreateBranchProtectionOption api.CreateBranchProtectionOption
// in:body
EditBranchProtectionOption api.EditBranchProtectionOption
}

View File

@ -36,6 +36,20 @@ type swaggerResponseBranchList struct {
Body []api.Branch `json:"body"`
}
// BranchProtection
// swagger:response BranchProtection
type swaggerResponseBranchProtection struct {
// in:body
Body api.BranchProtection `json:"body"`
}
// BranchProtectionList
// swagger:response BranchProtectionList
type swaggerResponseBranchProtectionList struct {
// in:body
Body []api.BranchProtection `json:"body"`
}
// TagList
// swagger:response TagList
type swaggerResponseTagList struct {

File diff suppressed because it is too large Load Diff