diff options
author | David Svantesson <davidsvantesson@gmail.com> | 2020-02-13 00:19:35 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-02-12 23:19:35 +0000 |
commit | 9ff4e1d2d9636ea8aa328427f1d31c962221263e (patch) | |
tree | b0df096e3885a6f05c26959f784cca0ce6a9763c /routers | |
parent | 908f8952be3ba7a4e4c32b0fd0dab5eb08ca8dd4 (diff) | |
download | gitea-9ff4e1d2d9636ea8aa328427f1d31c962221263e.tar.gz gitea-9ff4e1d2d9636ea8aa328427f1d31c962221263e.zip |
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>
Diffstat (limited to 'routers')
-rw-r--r-- | routers/api/v1/api.go | 9 | ||||
-rw-r--r-- | routers/api/v1/repo/branch.go | 506 | ||||
-rw-r--r-- | routers/api/v1/swagger/options.go | 6 | ||||
-rw-r--r-- | routers/api/v1/swagger/repo.go | 14 |
4 files changed, 533 insertions, 2 deletions
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0a352f6e46..0ddf57b743 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -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)) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 8f8ca15877..fccfc2bfe1 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -8,6 +8,7 @@ package repo import ( "net/http" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/git" @@ -71,7 +72,7 @@ func GetBranch(ctx *context.APIContext) { return } - ctx.JSON(http.StatusOK, convert.ToBranch(ctx.Repo.Repository, branch, c, branchProtection, ctx.User)) + ctx.JSON(http.StatusOK, convert.ToBranch(ctx.Repo.Repository, branch, c, branchProtection, ctx.User, ctx.Repo.IsAdmin())) } // ListBranches list all the branches of a repository @@ -114,8 +115,509 @@ func ListBranches(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err) return } - apiBranches[i] = convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.User) + apiBranches[i] = convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.User, ctx.Repo.IsAdmin()) } ctx.JSON(http.StatusOK, &apiBranches) } + +// GetBranchProtection gets a branch protection +func GetBranchProtection(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection + // --- + // summary: Get a specific branch protection for the repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: name + // in: path + // description: name of protected branch + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/BranchProtection" + // "404": + // "$ref": "#/responses/notFound" + + repo := ctx.Repo.Repository + bpName := ctx.Params(":name") + bp, err := models.GetProtectedBranchBy(repo.ID, bpName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) + return + } + if bp == nil || bp.RepoID != repo.ID { + ctx.NotFound() + return + } + + ctx.JSON(http.StatusOK, convert.ToBranchProtection(bp)) +} + +// ListBranchProtections list branch protections for a repo +func ListBranchProtections(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/branch_protections repository repoListBranchProtection + // --- + // summary: List branch protections for a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/BranchProtectionList" + + repo := ctx.Repo.Repository + bps, err := repo.GetProtectedBranches() + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedBranches", err) + return + } + apiBps := make([]*api.BranchProtection, len(bps)) + for i := range bps { + apiBps[i] = convert.ToBranchProtection(bps[i]) + } + + ctx.JSON(http.StatusOK, apiBps) +} + +// CreateBranchProtection creates a branch protection for a repo +func CreateBranchProtection(ctx *context.APIContext, form api.CreateBranchProtectionOption) { + // swagger:operation POST /repos/{owner}/{repo}/branch_protections repository repoCreateBranchProtection + // --- + // summary: Create a branch protections for a repository + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateBranchProtectionOption" + // responses: + // "201": + // "$ref": "#/responses/BranchProtection" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + repo := ctx.Repo.Repository + + // Currently protection must match an actual branch + if !git.IsBranchExist(ctx.Repo.Repository.RepoPath(), form.BranchName) { + ctx.NotFound() + return + } + + protectBranch, err := models.GetProtectedBranchBy(repo.ID, form.BranchName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectBranchOfRepoByName", err) + return + } else if protectBranch != nil { + ctx.Error(http.StatusForbidden, "Create branch protection", "Branch protection already exist") + return + } + + var requiredApprovals int64 + if form.RequiredApprovals > 0 { + requiredApprovals = form.RequiredApprovals + } + + whitelistUsers, err := models.GetUserIDsByNames(form.PushWhitelistUsernames, false) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + return + } + mergeWhitelistUsers, err := models.GetUserIDsByNames(form.MergeWhitelistUsernames, false) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + return + } + approvalsWhitelistUsers, err := models.GetUserIDsByNames(form.ApprovalsWhitelistUsernames, false) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + return + } + var whitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64 + if repo.Owner.IsOrganization() { + whitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.PushWhitelistTeams, false) + if err != nil { + if models.IsErrTeamNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + return + } + mergeWhitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.MergeWhitelistTeams, false) + if err != nil { + if models.IsErrTeamNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + return + } + approvalsWhitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.ApprovalsWhitelistTeams, false) + if err != nil { + if models.IsErrTeamNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + return + } + } + + protectBranch = &models.ProtectedBranch{ + RepoID: ctx.Repo.Repository.ID, + BranchName: form.BranchName, + CanPush: form.EnablePush, + EnableWhitelist: form.EnablePush && form.EnablePushWhitelist, + EnableMergeWhitelist: form.EnableMergeWhitelist, + WhitelistDeployKeys: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys, + EnableStatusCheck: form.EnableStatusCheck, + StatusCheckContexts: form.StatusCheckContexts, + EnableApprovalsWhitelist: form.EnableApprovalsWhitelist, + RequiredApprovals: requiredApprovals, + BlockOnRejectedReviews: form.BlockOnRejectedReviews, + DismissStaleApprovals: form.DismissStaleApprovals, + RequireSignedCommits: form.RequireSignedCommits, + } + + err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{ + UserIDs: whitelistUsers, + TeamIDs: whitelistTeams, + MergeUserIDs: mergeWhitelistUsers, + MergeTeamIDs: mergeWhitelistTeams, + ApprovalsUserIDs: approvalsWhitelistUsers, + ApprovalsTeamIDs: approvalsWhitelistTeams, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateProtectBranch", err) + return + } + + // Reload from db to get all whitelists + bp, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, form.BranchName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) + return + } + if bp == nil || bp.RepoID != ctx.Repo.Repository.ID { + ctx.Error(http.StatusInternalServerError, "New branch protection not found", err) + return + } + + ctx.JSON(http.StatusCreated, convert.ToBranchProtection(bp)) + +} + +// EditBranchProtection edits a branch protection for a repo +func EditBranchProtection(ctx *context.APIContext, form api.EditBranchProtectionOption) { + // swagger:operation PATCH /repos/{owner}/{repo}/branch_protections/{name} repository repoEditBranchProtection + // --- + // summary: Edit a branch protections for a repository. Only fields that are set will be changed + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: name + // in: path + // description: name of protected branch + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/EditBranchProtectionOption" + // responses: + // "200": + // "$ref": "#/responses/BranchProtection" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + repo := ctx.Repo.Repository + bpName := ctx.Params(":name") + protectBranch, err := models.GetProtectedBranchBy(repo.ID, bpName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) + return + } + if protectBranch == nil || protectBranch.RepoID != repo.ID { + ctx.NotFound() + return + } + + if form.EnablePush != nil { + if !*form.EnablePush { + protectBranch.CanPush = false + protectBranch.EnableWhitelist = false + protectBranch.WhitelistDeployKeys = false + } else { + protectBranch.CanPush = true + if form.EnablePushWhitelist != nil { + if !*form.EnablePushWhitelist { + protectBranch.EnableWhitelist = false + protectBranch.WhitelistDeployKeys = false + } else { + protectBranch.EnableWhitelist = true + if form.PushWhitelistDeployKeys != nil { + protectBranch.WhitelistDeployKeys = *form.PushWhitelistDeployKeys + } + } + } + } + } + + if form.EnableMergeWhitelist != nil { + protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist + } + + if form.EnableStatusCheck != nil { + protectBranch.EnableStatusCheck = *form.EnableStatusCheck + } + if protectBranch.EnableStatusCheck { + protectBranch.StatusCheckContexts = form.StatusCheckContexts + } + + if form.RequiredApprovals != nil && *form.RequiredApprovals >= 0 { + protectBranch.RequiredApprovals = *form.RequiredApprovals + } + + if form.EnableApprovalsWhitelist != nil { + protectBranch.EnableApprovalsWhitelist = *form.EnableApprovalsWhitelist + } + + if form.BlockOnRejectedReviews != nil { + protectBranch.BlockOnRejectedReviews = *form.BlockOnRejectedReviews + } + + if form.DismissStaleApprovals != nil { + protectBranch.DismissStaleApprovals = *form.DismissStaleApprovals + } + + if form.RequireSignedCommits != nil { + protectBranch.RequireSignedCommits = *form.RequireSignedCommits + } + + var whitelistUsers []int64 + if form.PushWhitelistUsernames != nil { + whitelistUsers, err = models.GetUserIDsByNames(form.PushWhitelistUsernames, false) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + return + } + } else { + whitelistUsers = protectBranch.WhitelistUserIDs + } + var mergeWhitelistUsers []int64 + if form.MergeWhitelistUsernames != nil { + mergeWhitelistUsers, err = models.GetUserIDsByNames(form.MergeWhitelistUsernames, false) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + return + } + } else { + mergeWhitelistUsers = protectBranch.MergeWhitelistUserIDs + } + var approvalsWhitelistUsers []int64 + if form.ApprovalsWhitelistUsernames != nil { + approvalsWhitelistUsers, err = models.GetUserIDsByNames(form.ApprovalsWhitelistUsernames, false) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + return + } + } else { + approvalsWhitelistUsers = protectBranch.ApprovalsWhitelistUserIDs + } + + var whitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64 + if repo.Owner.IsOrganization() { + if form.PushWhitelistTeams != nil { + whitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.PushWhitelistTeams, false) + if err != nil { + if models.IsErrTeamNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + return + } + } else { + whitelistTeams = protectBranch.WhitelistTeamIDs + } + if form.MergeWhitelistTeams != nil { + mergeWhitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.MergeWhitelistTeams, false) + if err != nil { + if models.IsErrTeamNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + return + } + } else { + mergeWhitelistTeams = protectBranch.MergeWhitelistTeamIDs + } + if form.ApprovalsWhitelistTeams != nil { + approvalsWhitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.ApprovalsWhitelistTeams, false) + if err != nil { + if models.IsErrTeamNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + return + } + } else { + approvalsWhitelistTeams = protectBranch.ApprovalsWhitelistTeamIDs + } + } + + err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{ + UserIDs: whitelistUsers, + TeamIDs: whitelistTeams, + MergeUserIDs: mergeWhitelistUsers, + MergeTeamIDs: mergeWhitelistTeams, + ApprovalsUserIDs: approvalsWhitelistUsers, + ApprovalsTeamIDs: approvalsWhitelistTeams, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateProtectBranch", err) + return + } + + // Reload from db to ensure get all whitelists + bp, err := models.GetProtectedBranchBy(repo.ID, bpName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err) + return + } + if bp == nil || bp.RepoID != ctx.Repo.Repository.ID { + ctx.Error(http.StatusInternalServerError, "New branch protection not found", err) + return + } + + ctx.JSON(http.StatusOK, convert.ToBranchProtection(bp)) +} + +// DeleteBranchProtection deletes a branch protection for a repo +func DeleteBranchProtection(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/branch_protections/{name} repository repoDeleteBranchProtection + // --- + // summary: Delete a specific branch protection for the repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: name + // in: path + // description: name of protected branch + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + repo := ctx.Repo.Repository + bpName := ctx.Params(":name") + bp, err := models.GetProtectedBranchBy(repo.ID, bpName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) + return + } + if bp == nil || bp.RepoID != repo.ID { + ctx.NotFound() + return + } + + if err := ctx.Repo.Repository.DeleteProtectedBranch(bp.ID); err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteProtectedBranch", err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index ab697811d0..679b4aa708 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -128,4 +128,10 @@ type swaggerParameterBodies struct { // in:body EditReactionOption api.EditReactionOption + + // in:body + CreateBranchProtectionOption api.CreateBranchProtectionOption + + // in:body + EditBranchProtectionOption api.EditBranchProtectionOption } diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index 4ac5c6d2d5..2a657f3122 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -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 { |