gitea_hat/routers/hat/hat.go

507 lines
16 KiB
Go

package hat
import (
gocontext "context"
"fmt"
"net/http"
"reflect"
"strings"
gitea_api "code.gitea.io/gitea/modules/structs"
hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs"
"gitea.com/go-chi/binding"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/misc"
"code.gitea.io/gitea/services/auth"
context_service "code.gitea.io/gitea/services/context"
"code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/admin"
"code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/org"
"code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/repo"
"code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/user"
"github.com/go-chi/cors"
)
func Routers(ctx gocontext.Context) *web.Route {
m := web.NewRoute()
m.Use(securityHeaders())
if setting.CORSConfig.Enabled {
m.Use(cors.Handler(cors.Options{
// Scheme: setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option
AllowedOrigins: setting.CORSConfig.AllowDomain,
// setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option
AllowedMethods: setting.CORSConfig.Methods,
AllowCredentials: setting.CORSConfig.AllowCredentials,
AllowedHeaders: []string{"Authorization", "X-Gitea-OTP"},
MaxAge: int(setting.CORSConfig.MaxAge.Seconds()),
}))
}
m.Use(context.APIContexter())
group := buildAuthGroup()
if err := group.Init(ctx); err != nil {
log.Error("Could not initialize '%s' auth method, error: %s", group.Name(), err)
}
// Get user from session if logged in.
m.Use(context.APIAuth(group))
m.Use(context.ToggleAPI(&context.ToggleOptions{
SignInRequired: false,
}))
reqRepoCodeReader := context.RequireRepoReader(unit_model.TypeCode)
m.Group("", func() {
m.Get("/version", misc.Version)
m.Post("/create_pr_version", bind(gitea_api.PullRequestPayload{}), repo.CreatePrVersion)
m.Group("/repos", func() {
m.Group("/{username}/{reponame}", func() {
m.Post("/transfer", reqOwner(), bind(gitea_api.TransferRepoOption{}), repo.Transfer)
m.Get("/branch_name_set", context.ReferencesGitRepo(), repo.BranchNameSet)
m.Get("/tag_name_set", context.ReferencesGitRepo(), repo.TagNameSet)
m.Get("/branch_tag_count", context.ReferencesGitRepo(), repo.BranchTagCount)
m.Group("/branches", func() {
m.Get("", context.ReferencesGitRepo(), repo.ListBranches)
m.Get("/branches_slice", context.ReferencesGitRepo(), repo.ListBranchesSlice)
}, reqRepoReader(unit_model.TypeCode))
m.Group("/commits", func() {
m.Get("/{sha}/diff", repo.GetCommitDiff)
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
m.Group("/tags", func() {
m.Get("", repo.ListTags)
}, reqRepoReader(unit_model.TypeCode), context.ReferencesGitRepo(true))
m.Group("/wikies", func() {
m.Combo("").Get(repo.ListWikiPages).
Post(bind(hat_api.WikiOption{}), repo.CreateWiki)
m.Group("/{page}", func() {
m.Combo("").Get(repo.GetWiki).
Patch(bind(hat_api.WikiOption{}), repo.EditWiki).
Delete(repo.DeleteWiki)
})
})
m.Group("/readme", func() {
m.Get("", repo.GetReadmeContents)
m.Get("/*", repo.GetReadmeContentsByPath)
})
m.Get("/commits_slice", repo.GetAllCommitsSliceByTime)
m.Get("/compare/*", repo.MustBeNotEmpty, reqRepoCodeReader,
repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.CompareDiff)
m.Group("/pulls", func() {
m.Group("/{index}", func() {
m.Combo("").Get(repo.GetPullRequest).
Patch(bind(gitea_api.EditPullRequestOption{}), repo.EditPullRequest)
m.Get("/commits", context.RepoRef(), repo.GetPullCommits)
m.Get("/files", context.RepoRef(), repo.GetPullFiles)
m.Group("/versions", func() {
m.Get("", repo.ListPullRequestVersions)
m.Get("/{versionId}/diff", context.RepoRef(), repo.GetPullRequestVersionDiff)
})
})
}, mustAllowPulls, reqRepoReader(unit_model.TypeCode), context.ReferencesGitRepo())
m.Group("/releases", func() {
m.Combo("").Get(repo.ListReleases).
Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(gitea_api.CreateReleaseOption{}), repo.CreateRelease)
m.Group("/{id}", func() {
m.Combo("").Get(repo.GetRelease).
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(gitea_api.EditReleaseOption{}), repo.EditRelease)
})
m.Get("/latest", context.ReferencesGitRepo(), repo.GetLatestRelease)
}, reqRepoReader(unit.TypeReleases))
m.Group("/contributors", func() {
m.Get("", context.ReferencesGitRepo(), repo.GetContributors)
m.Get("/stat", context.ReferencesGitRepo(), repo.GetContributorStat)
})
m.Group("/count", func() {
m.Get("", context.ReferencesGitRepo(), repo.GetCommitCount)
})
m.Group("/file_commits", func() {
m.Get("/*", context.ReferencesGitRepo(), repo.GetFileAllCommits)
})
m.Group("/hooks", func() {
m.Combo("").Post(bind(hat_api.CreateHookOption{}), repo.CreateHook)
m.Group("/{id}", func() {
m.Combo("").Patch(bind(hat_api.EditHookOption{}), repo.EditHook)
m.Get("/hooktasks", repo.ListHookTask)
})
}, reqToken(), reqAdmin(), reqWebhooksEnabled())
m.Group("/contents", func() {
m.Get("", repo.GetContentsList)
m.Get("/*", repo.GetContents)
m.Group("/batch", func() {
m.Post("", bind(hat_api.BatchChangeFileOptions{}), repo.BatchChangeFile)
}, reqRepoWriter(unit.TypeCode), reqToken())
}, reqRepoReader(unit.TypeCode))
m.Get("/find", context.RepoRef(), reqRepoReader(unit.TypeCode), repo.FindFiles)
m.Group("/git", func() {
m.Group("/commits", func() {
m.Get("/{sha}", repo.GetSingleCommit)
})
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
m.Get("/blame", context.RepoRef(), repo.GetRepoRefBlame)
m.Get("/code_stats", context.RepoRef(), repo.ListCodeStats)
}, repoAssignment())
})
m.Group("/users", func() {
m.Group("/{username}", func() {
if setting.Service.EnableUserHeatmap {
m.Get("/heatmap", user.GetUserHeatmapData)
}
}, context_service.UserAssignmentAPI())
})
m.Post("/orgs", reqToken(), bind(gitea_api.CreateOrgOption{}), org.Create)
m.Group("/orgs/{org}", func() {
m.Combo("").Patch(reqToken(), reqOrgOwnership(), bind(hat_api.EditOrgOption{}), org.Edit)
}, orgAssignment(true))
m.Group("/teams/{teamid}", func() {
m.Group("/repos", func() {
m.Put("/{org}", org.AddTeamAllRepository)
m.Delete("/{org}", org.RemoveTeamAllRepository)
})
}, orgAssignment(false, true), reqToken(), reqTeamMembership())
m.Group("/admin", func() {
m.Group("/users", func() {
m.Group("/{username}", func() {
m.Combo("").Patch(bind(hat_api.HatEditUserOption{}), admin.EditUser)
}, context_service.UserAssignmentAPI())
})
}, reqToken(), reqSiteAdmin())
})
return m
}
func securityHeaders() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
// CORB: https://www.chromium.org/Home/chromium-security/corb-for-developers
// http://stackoverflow.com/a/3146618/244009
resp.Header().Set("x-content-type-options", "nosniff")
next.ServeHTTP(resp, req)
})
}
}
func buildAuthGroup() *auth.Group {
group := auth.NewGroup(
&auth.OAuth2{},
&auth.HTTPSign{},
&auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API
)
if setting.Service.EnableReverseProxyAuth {
group.Add(&auth.ReverseProxy{})
}
specialAdd(group)
return group
}
func repoAssignment() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
userName := ctx.Params("username")
repoName := ctx.Params("reponame")
var (
owner *user_model.User
err error
)
// Check if the user is the same as the repository owner.
if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) {
owner = ctx.Doer
} else {
owner, err = user_model.GetUserByName(ctx, userName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err := user_model.LookupUserRedirect(userName); err == nil {
context.RedirectToUser(ctx.Context, userName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound("GetUserByName", err)
} else {
ctx.Error(http.StatusInternalServerError, "LookupUserRedirect", err)
}
} else {
ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
}
return
}
}
ctx.Repo.Owner = owner
ctx.ContextUser = owner
// Get repository.
repo, err := repo_model.GetRepositoryByName(owner.ID, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
redirectRepoID, err := repo_model.LookupRedirect(owner.ID, repoName)
if err == nil {
context.RedirectToRepo(ctx.Context, redirectRepoID)
} else if repo_model.IsErrRedirectNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "LookupRepoRedirect", err)
}
} else {
ctx.Error(http.StatusInternalServerError, "GetRepositoryByName", err)
}
return
}
repo.Owner = owner
ctx.Repo.Repository = repo
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return
}
if !ctx.Repo.HasAccess() {
ctx.NotFound()
return
}
}
}
func orgAssignment(args ...bool) func(ctx *context.APIContext) {
var (
assignOrg bool
assignTeam bool
)
if len(args) > 0 {
assignOrg = args[0]
}
if len(args) > 1 {
assignTeam = args[1]
}
return func(ctx *context.APIContext) {
ctx.Org = new(context.APIOrganization)
var err error
if assignOrg {
ctx.Org.Organization, err = organization.GetOrgByName(ctx.Params(":org"))
if err != nil {
if organization.IsErrOrgNotExist(err) {
redirectUserID, err := user_model.LookupUserRedirect(ctx.Params(":org"))
if err == nil {
context.RedirectToUser(ctx.Context, ctx.Params(":org"), redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound("GetOrgByName", err)
} else {
ctx.Error(http.StatusInternalServerError, "LookupUserRedirect", err)
}
} else {
ctx.Error(http.StatusInternalServerError, "GetOrgByName", err)
}
return
}
ctx.ContextUser = ctx.Org.Organization.AsUser()
}
if assignTeam {
ctx.Org.Team, err = organization.GetTeamByID(ctx, ctx.ParamsInt64(":teamid"))
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetTeamById", err)
}
return
}
}
}
}
// bind binding an obj to a func(ctx *context.APIContext)
func bind(obj interface{}) http.HandlerFunc {
tp := reflect.TypeOf(obj)
for tp.Kind() == reflect.Ptr {
tp = tp.Elem()
}
return web.Wrap(func(ctx *context.APIContext) {
theObj := reflect.New(tp).Interface() // create a new form obj for every request but not use obj directly
errs := binding.Bind(ctx.Req, theObj)
if len(errs) > 0 {
ctx.Error(http.StatusUnprocessableEntity, "validationError", fmt.Sprintf("%s: %s", errs[0].FieldNames, errs[0].Error()))
return
}
web.SetForm(ctx, theObj)
})
}
func reqOwner() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.IsUserRepoOwner() && !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqOwner", "user should be the owner of the repo")
return
}
}
}
func reqAdmin() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqAdmin", "user should be an owner or a collaborator with admin write of a repository")
return
}
}
}
// reqRepoReader user should have specific read permission or be a repo admin or a site admin
func reqRepoReader(unitType unit_model.Type) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.IsUserRepoReaderSpecific(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin")
return
}
}
}
// reqRepoWriter user should have a permission to write to a repo, or be a site admin
func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.IsUserRepoWriter(unitTypes) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqRepoWriter", "user should have a permission to write to a repo")
return
}
}
}
func reqOrgOwnership() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if ctx.Context.IsUserSiteAdmin() {
return
}
var orgID int64
if ctx.Org.Organization != nil {
orgID = ctx.Org.Organization.ID
} else if ctx.Org.Team != nil {
orgID = ctx.Org.Team.OrgID
} else {
ctx.Error(http.StatusInternalServerError, "", "reqOrgOwnership: unprepared context")
return
}
isOwner, err := organization.IsOrganizationOwner(ctx, orgID, ctx.Doer.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "IsOrganizationOwner", err)
return
} else if !isOwner {
if ctx.Org.Organization != nil {
ctx.Error(http.StatusForbidden, "", "Must be an organization owner")
} else {
ctx.NotFound()
}
return
}
}
}
// reqTeamMembership user should be an team member, or a site admin
func reqTeamMembership() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if ctx.Context.IsUserSiteAdmin() {
return
}
if ctx.Org.Team == nil {
ctx.Error(http.StatusInternalServerError, "", "reqTeamMembership: unprepared context")
return
}
orgID := ctx.Org.Team.OrgID
isOwner, err := organization.IsOrganizationOwner(ctx, orgID, ctx.Doer.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "IsOrganizationOwner", err)
return
} else if isOwner {
return
}
if isTeamMember, err := organization.IsTeamMember(ctx, orgID, ctx.Org.Team.ID, ctx.Doer.ID); err != nil {
ctx.Error(http.StatusInternalServerError, "IsTeamMember", err)
return
} else if !isTeamMember {
isOrgMember, err := organization.IsOrganizationMember(ctx, orgID, ctx.Doer.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err)
} else if isOrgMember {
ctx.Error(http.StatusForbidden, "", "Must be a team member")
} else {
ctx.NotFound()
}
return
}
}
}
func reqWebhooksEnabled() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if setting.DisableWebhooks {
ctx.Error(http.StatusForbidden, "", "webhooks disabled by administrator")
return
}
}
}
func mustAllowPulls(ctx *context.APIContext) {
if !(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.CanRead(unit.TypePullRequests)) {
if ctx.Repo.Repository.CanEnablePulls() && log.IsTrace() {
if ctx.IsSigned {
log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
"User in Repo has Permissions: %-+v",
ctx.Doer,
unit.TypePullRequests,
ctx.Repo.Repository,
ctx.Repo.Permission)
} else {
log.Trace("Permission Denied: Anonymous user cannot read %-v in Repo %-v\n"+
"Anonymous user in Repo has Permissions: %-+v",
unit.TypePullRequests,
ctx.Repo.Repository,
ctx.Repo.Permission)
}
}
ctx.NotFound()
return
}
}
func reqToken() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if true == ctx.Data["IsApiToken"] {
return
}
if ctx.Context.IsBasicAuth {
ctx.CheckForOTP()
return
}
if ctx.IsSigned {
return
}
ctx.Error(http.StatusUnauthorized, "reqToken", "token is required")
}
}
// reqSiteAdmin user should be the site admin
func reqSiteAdmin() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqSiteAdmin", "user should be the site admin")
return
}
}
}