507 lines
16 KiB
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
|
|
}
|
|
}
|
|
}
|