diff options
Diffstat (limited to 'routers/api/v1/api.go')
-rw-r--r-- | routers/api/v1/api.go | 1659 |
1 files changed, 1659 insertions, 0 deletions
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go new file mode 100644 index 0000000..6e4a97b --- /dev/null +++ b/routers/api/v1/api.go @@ -0,0 +1,1659 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2016 The Gitea Authors. All rights reserved. +// Copyright 2023-2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// Package v1 Gitea API +// +// This documentation describes the Gitea API. +// +// Schemes: https, http +// BasePath: /api/v1 +// Version: {{AppVer | JSEscape}} +// License: MIT http://opensource.org/licenses/MIT +// +// Consumes: +// - application/json +// - text/plain +// +// Produces: +// - application/json +// - text/html +// +// Security: +// - BasicAuth : +// - Token : +// - AccessToken : +// - AuthorizationHeaderToken : +// - SudoParam : +// - SudoHeader : +// - TOTPHeader : +// +// SecurityDefinitions: +// BasicAuth: +// type: basic +// Token: +// type: apiKey +// name: token +// in: query +// description: This authentication option is deprecated for removal in Gitea 1.23. Please use AuthorizationHeaderToken instead. +// AccessToken: +// type: apiKey +// name: access_token +// in: query +// description: This authentication option is deprecated for removal in Gitea 1.23. Please use AuthorizationHeaderToken instead. +// AuthorizationHeaderToken: +// type: apiKey +// name: Authorization +// in: header +// description: API tokens must be prepended with "token" followed by a space. +// SudoParam: +// type: apiKey +// name: sudo +// in: query +// description: Sudo API request as the user provided as the key. Admin privileges are required. +// SudoHeader: +// type: apiKey +// name: Sudo +// in: header +// description: Sudo API request as the user provided as the key. Admin privileges are required. +// TOTPHeader: +// type: apiKey +// name: X-FORGEJO-OTP +// in: header +// description: Must be used in combination with BasicAuth if two-factor authentication is enabled. +// +// swagger:meta +package v1 + +import ( + "fmt" + "net/http" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + quota_model "code.gitea.io/gitea/models/quota" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/forgefed" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/shared" + "code.gitea.io/gitea/routers/api/v1/activitypub" + "code.gitea.io/gitea/routers/api/v1/admin" + "code.gitea.io/gitea/routers/api/v1/misc" + "code.gitea.io/gitea/routers/api/v1/notify" + "code.gitea.io/gitea/routers/api/v1/org" + "code.gitea.io/gitea/routers/api/v1/packages" + "code.gitea.io/gitea/routers/api/v1/repo" + "code.gitea.io/gitea/routers/api/v1/settings" + "code.gitea.io/gitea/routers/api/v1/user" + "code.gitea.io/gitea/services/actions" + "code.gitea.io/gitea/services/auth" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + + _ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation + + "gitea.com/go-chi/binding" +) + +func sudo() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + sudo := ctx.FormString("sudo") + if len(sudo) == 0 { + sudo = ctx.Req.Header.Get("Sudo") + } + + if len(sudo) > 0 { + if ctx.IsSigned && ctx.Doer.IsAdmin { + user, err := user_model.GetUserByName(ctx, sudo) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + } + return + } + log.Trace("Sudo from (%s) to: %s", ctx.Doer.Name, user.Name) + ctx.Doer = user + } else { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only administrators allowed to sudo.", + }) + return + } + } + } +} + +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(ctx, userName); err == nil { + context.RedirectToUser(ctx.Base, 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(ctx, owner.ID, repoName) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, repoName) + if err == nil { + context.RedirectToRepo(ctx.Base, 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 + + if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID { + taskID := ctx.Data["ActionsTaskID"].(int64) + task, err := actions_model.GetTaskByID(ctx, taskID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "actions_model.GetTaskByID", err) + return + } + if task.RepoID != repo.ID { + ctx.NotFound() + return + } + + if task.IsForkPullRequest { + ctx.Repo.Permission.AccessMode = perm.AccessModeRead + } else { + ctx.Repo.Permission.AccessMode = perm.AccessModeWrite + } + + if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadUnits", err) + return + } + ctx.Repo.Permission.Units = ctx.Repo.Repository.Units + ctx.Repo.Permission.UnitsMode = make(map[unit.Type]perm.AccessMode) + for _, u := range ctx.Repo.Repository.Units { + ctx.Repo.Permission.UnitsMode[u.Type] = ctx.Repo.Permission.AccessMode + } + } else { + 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 + } + } +} + +// must be used within a group with a call to repoAssignment() to set ctx.Repo +func commentAssignment(idParam string) func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(idParam)) + if err != nil { + if issues_model.IsErrCommentNotExist(err) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + if err = comment.LoadIssue(ctx); err != nil { + ctx.InternalServerError(err) + return + } + if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound() + return + } + + if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { + ctx.NotFound() + return + } + + comment.Issue.Repo = ctx.Repo.Repository + + ctx.Comment = comment + } +} + +func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { + ctx.Error(http.StatusForbidden, "reqPackageAccess", "user should have specific permission or be a site admin") + return + } + } +} + +func checkTokenPublicOnly() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if !ctx.PublicOnly { + return + } + + requiredScopeCategories, ok := ctx.Data["requiredScopeCategories"].([]auth_model.AccessTokenScopeCategory) + if !ok || len(requiredScopeCategories) == 0 { + return + } + + // public Only permission check + switch { + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository): + if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos") + return + } + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue): + if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public issues") + return + } + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization): + if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs") + return + } + if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs") + return + } + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser): + if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public users") + return + } + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub): + if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public activitypub") + return + } + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification): + if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public notifications") + return + } + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage): + if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public packages") + return + } + } + } +} + +// if a token is being used for auth, we check that it contains the required scope +// if a token is not being used, reqToken will enforce other sign in methods +func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + // no scope required + if len(requiredScopeCategories) == 0 { + return + } + + // Need OAuth2 token to be present. + scope, scopeExists := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + if ctx.Data["IsApiToken"] != true || !scopeExists { + return + } + + // use the http method to determine the access level + requiredScopeLevel := auth_model.Read + if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" { + requiredScopeLevel = auth_model.Write + } + + // get the required scope for the given access level and category + requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...) + allow, err := scope.HasScope(requiredScopes...) + if err != nil { + ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error()) + return + } + + if !allow { + ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes)) + return + } + + ctx.Data["requiredScopeCategories"] = requiredScopeCategories + + // check if scope only applies to public resources + publicOnly, err := scope.PublicOnly() + if err != nil { + ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) + return + } + + // assign to true so that those searching should only filter public repositories/users/organizations + ctx.PublicOnly = publicOnly + } +} + +// Contexter middleware already checks token for user sign in process. +func reqToken() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + // If actions token is present + if true == ctx.Data["IsActionsToken"] { + return + } + + if ctx.IsSigned { + return + } + ctx.Error(http.StatusUnauthorized, "reqToken", "token is required") + } +} + +func reqExploreSignIn() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if setting.Service.Explore.RequireSigninView && !ctx.IsSigned { + ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users") + } + } +} + +func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if ctx.IsSigned && setting.Service.EnableReverseProxyAuthAPI && ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName { + return + } + if !ctx.IsBasicAuth { + ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required") + return + } + } +} + +// 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 + } + } +} + +// reqOwner user should be the owner of the repo or site admin. +func reqOwner() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if !ctx.Repo.IsOwner() && !ctx.IsUserSiteAdmin() { + ctx.Error(http.StatusForbidden, "reqOwner", "user should be the owner of the repo") + return + } + } +} + +// reqSelfOrAdmin doer should be the same as the contextUser or site admin +func reqSelfOrAdmin() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if !ctx.IsUserSiteAdmin() && ctx.ContextUser != ctx.Doer { + ctx.Error(http.StatusForbidden, "reqSelfOrAdmin", "doer should be the site admin or be same as the contextUser") + return + } + } +} + +// reqAdmin user should be an owner or a collaborator with admin write of a repository, or site admin +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 + } + } +} + +// 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 + } + } +} + +// reqRepoBranchWriter user should have a permission to write to a branch, or be a site admin +func reqRepoBranchWriter(ctx *context.APIContext) { + options, ok := web.GetForm(ctx).(api.FileOptionInterface) + if !ok || (!ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, options.Branch()) && !ctx.IsUserSiteAdmin()) { + ctx.Error(http.StatusForbidden, "reqRepoBranchWriter", "user should have a permission to write to this branch") + return + } +} + +// reqRepoReader user should have specific read permission or be a repo admin or a site admin +func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if !ctx.Repo.CanRead(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 + } + } +} + +// reqAnyRepoReader user should have any permission to read repository or permissions of site admin +func reqAnyRepoReader() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if !ctx.Repo.HasAccess() && !ctx.IsUserSiteAdmin() { + ctx.Error(http.StatusForbidden, "reqAnyRepoReader", "user should have any permission to read repository or permissions of site admin") + return + } + } +} + +// reqOrgOwnership user should be an organization owner, or a site admin +func reqOrgOwnership() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if ctx.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.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 + } + } +} + +// reqOrgMembership user should be an organization member, or a site admin +func reqOrgMembership() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if ctx.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, "", "reqOrgMembership: unprepared context") + return + } + + if isMember, err := organization.IsOrganizationMember(ctx, orgID, ctx.Doer.ID); err != nil { + ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err) + return + } else if !isMember { + if ctx.Org.Organization != nil { + ctx.Error(http.StatusForbidden, "", "Must be an organization member") + } else { + ctx.NotFound() + } + return + } + } +} + +func reqGitHook() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if !ctx.Doer.CanEditGitHook() { + ctx.Error(http.StatusForbidden, "", "must be allowed to edit Git hooks") + return + } + } +} + +// reqWebhooksEnabled requires webhooks to be enabled by admin. +func reqWebhooksEnabled() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if setting.DisableWebhooks { + ctx.Error(http.StatusForbidden, "", "webhooks disabled by administrator") + 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, ctx.Params(":org")) + if err != nil { + if organization.IsErrOrgNotExist(err) { + redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.Params(":org")) + if err == nil { + context.RedirectToUser(ctx.Base, 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 + } + } + } +} + +func mustEnableIssues(ctx *context.APIContext) { + if !ctx.Repo.CanRead(unit.TypeIssues) { + if 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.TypeIssues, + 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.TypeIssues, + ctx.Repo.Repository, + ctx.Repo.Permission) + } + } + ctx.NotFound() + 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 mustEnableIssuesOrPulls(ctx *context.APIContext) { + if !ctx.Repo.CanRead(unit.TypeIssues) && + !(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 and %-v in Repo %-v\n"+ + "User in Repo has Permissions: %-+v", + ctx.Doer, + unit.TypeIssues, + unit.TypePullRequests, + ctx.Repo.Repository, + ctx.Repo.Permission) + } else { + log.Trace("Permission Denied: Anonymous user cannot read %-v and %-v in Repo %-v\n"+ + "Anonymous user in Repo has Permissions: %-+v", + unit.TypeIssues, + unit.TypePullRequests, + ctx.Repo.Repository, + ctx.Repo.Permission) + } + } + ctx.NotFound() + return + } +} + +func mustEnableWiki(ctx *context.APIContext) { + if !(ctx.Repo.CanRead(unit.TypeWiki)) { + ctx.NotFound() + return + } +} + +func mustNotBeArchived(ctx *context.APIContext) { + if ctx.Repo.Repository.IsArchived { + ctx.Error(http.StatusLocked, "RepoArchived", fmt.Errorf("%s is archived", ctx.Repo.Repository.LogString())) + return + } +} + +func mustEnableAttachments(ctx *context.APIContext) { + if !setting.Attachment.Enabled { + ctx.NotFound() + return + } +} + +// bind binding an obj to a func(ctx *context.APIContext) +func bind[T any](_ T) any { + return func(ctx *context.APIContext) { + theObj := new(T) // 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 individualPermsChecker(ctx *context.APIContext) { + // org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked. + if ctx.ContextUser.IsIndividual() { + switch { + case ctx.ContextUser.Visibility == api.VisibleTypePrivate: + if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) { + ctx.NotFound("Visit Project", nil) + return + } + case ctx.ContextUser.Visibility == api.VisibleTypeLimited: + if ctx.Doer == nil { + ctx.NotFound("Visit Project", nil) + return + } + } + } +} + +// Routes registers all v1 APIs routes to web application. +func Routes() *web.Route { + m := web.NewRoute() + + m.Use(shared.Middlewares()...) + + addActionsRoutes := func( + m *web.Route, + reqChecker func(ctx *context.APIContext), + act actions.API, + ) { + m.Group("/actions", func() { + m.Group("/secrets", func() { + m.Get("", reqToken(), reqChecker, act.ListActionsSecrets) + m.Combo("/{secretname}"). + Put(reqToken(), reqChecker, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret). + Delete(reqToken(), reqChecker, act.DeleteSecret) + }) + + m.Group("/variables", func() { + m.Get("", reqToken(), reqChecker, act.ListVariables) + m.Combo("/{variablename}"). + Get(reqToken(), reqChecker, act.GetVariable). + Delete(reqToken(), reqChecker, act.DeleteVariable). + Post(reqToken(), reqChecker, bind(api.CreateVariableOption{}), act.CreateVariable). + Put(reqToken(), reqChecker, bind(api.UpdateVariableOption{}), act.UpdateVariable) + }) + + m.Group("/runners", func() { + m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken) + }) + }) + } + + m.Group("", func() { + // Miscellaneous (no scope required) + if setting.API.EnableSwagger { + m.Get("/swagger", func(ctx *context.APIContext) { + ctx.Redirect(setting.AppSubURL + "/api/swagger") + }) + } + + if setting.Federation.Enabled { + m.Get("/nodeinfo", misc.NodeInfo) + m.Group("/activitypub", func() { + // deprecated, remove in 1.20, use /user-id/{user-id} instead + m.Group("/user/{username}", func() { + m.Get("", activitypub.Person) + m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox) + }, context.UserAssignmentAPI(), checkTokenPublicOnly()) + m.Group("/user-id/{user-id}", func() { + m.Get("", activitypub.Person) + m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox) + }, context.UserIDAssignmentAPI(), checkTokenPublicOnly()) + m.Group("/actor", func() { + m.Get("", activitypub.Actor) + m.Post("/inbox", activitypub.ActorInbox) + }) + m.Group("/repository-id/{repository-id}", func() { + m.Get("", activitypub.Repository) + m.Post("/inbox", + bind(forgefed.ForgeLike{}), + // TODO: activitypub.ReqHTTPSignature(), + activitypub.RepositoryInbox) + }, context.RepositoryIDAssignmentAPI()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub)) + } + + // Misc (public accessible) + m.Group("", func() { + m.Get("/version", misc.Version) + m.Get("/signing-key.gpg", misc.SigningKey) + m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) + m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown) + m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw) + m.Get("/gitignore/templates", misc.ListGitignoresTemplates) + m.Get("/gitignore/templates/{name}", misc.GetGitignoreTemplateInfo) + m.Get("/licenses", misc.ListLicenseTemplates) + m.Get("/licenses/{name}", misc.GetLicenseTemplateInfo) + m.Get("/label/templates", misc.ListLabelTemplates) + m.Get("/label/templates/{name}", misc.GetLabelTemplate) + + m.Group("/settings", func() { + m.Get("/ui", settings.GetGeneralUISettings) + m.Get("/api", settings.GetGeneralAPISettings) + m.Get("/attachment", settings.GetGeneralAttachmentSettings) + m.Get("/repository", settings.GetGeneralRepoSettings) + }) + }) + + // Notifications (requires 'notifications' scope) + m.Group("/notifications", func() { + m.Combo(""). + Get(reqToken(), notify.ListNotifications). + Put(reqToken(), notify.ReadNotifications) + m.Get("/new", reqToken(), notify.NewAvailable) + m.Combo("/threads/{id}"). + Get(reqToken(), notify.GetThread). + Patch(reqToken(), notify.ReadThread) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification)) + + // Users (requires user scope) + m.Group("/users", func() { + m.Get("/search", reqExploreSignIn(), user.Search) + + m.Group("/{username}", func() { + m.Get("", reqExploreSignIn(), user.GetInfo) + + if setting.Service.EnableUserHeatmap { + m.Get("/heatmap", user.GetUserHeatmapData) + } + + m.Get("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqExploreSignIn(), user.ListUserRepos) + m.Group("/tokens", func() { + m.Combo("").Get(user.ListAccessTokens). + Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken) + m.Combo("/{id}").Delete(reqToken(), user.DeleteAccessToken) + }, reqSelfOrAdmin(), reqBasicOrRevProxyAuth()) + + m.Get("/activities/feeds", user.ListUserActivityFeeds) + }, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser)) + + // Users (requires user scope) + m.Group("/users", func() { + m.Group("/{username}", func() { + m.Get("/keys", user.ListPublicKeys) + m.Get("/gpg_keys", user.ListGPGKeys) + + m.Get("/followers", user.ListFollowers) + m.Group("/following", func() { + m.Get("", user.ListFollowing) + m.Get("/{target}", user.CheckFollowing) + }) + + if !setting.Repository.DisableStars { + m.Get("/starred", user.GetStarredRepos) + } + + m.Get("/subscriptions", user.GetWatchedRepos) + }, context.UserAssignmentAPI(), checkTokenPublicOnly()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) + + // Users (requires user scope) + m.Group("/user", func() { + m.Get("", user.GetAuthenticatedUser) + if setting.Quota.Enabled { + m.Group("/quota", func() { + m.Get("", user.GetQuota) + m.Get("/check", user.CheckQuota) + m.Get("/attachments", user.ListQuotaAttachments) + m.Get("/packages", user.ListQuotaPackages) + m.Get("/artifacts", user.ListQuotaArtifacts) + }) + } + m.Group("/settings", func() { + m.Get("", user.GetUserSettings) + m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings) + }, reqToken()) + m.Combo("/emails"). + Get(user.ListEmails). + Post(bind(api.CreateEmailOption{}), user.AddEmail). + Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail) + + // manage user-level actions features + m.Group("/actions", func() { + m.Group("/secrets", func() { + m.Combo("/{secretname}"). + Put(bind(api.CreateOrUpdateSecretOption{}), user.CreateOrUpdateSecret). + Delete(user.DeleteSecret) + }) + + m.Group("/variables", func() { + m.Get("", user.ListVariables) + m.Combo("/{variablename}"). + Get(user.GetVariable). + Delete(user.DeleteVariable). + Post(bind(api.CreateVariableOption{}), user.CreateVariable). + Put(bind(api.UpdateVariableOption{}), user.UpdateVariable) + }) + + m.Group("/runners", func() { + m.Get("/registration-token", reqToken(), user.GetRegistrationToken) + }) + }) + + m.Get("/followers", user.ListMyFollowers) + m.Group("/following", func() { + m.Get("", user.ListMyFollowing) + m.Group("/{username}", func() { + m.Get("", user.CheckMyFollowing) + m.Put("", user.Follow) + m.Delete("", user.Unfollow) + }, context.UserAssignmentAPI()) + }) + + // (admin:public_key scope) + m.Group("/keys", func() { + m.Combo("").Get(user.ListMyPublicKeys). + Post(bind(api.CreateKeyOption{}), user.CreatePublicKey) + m.Combo("/{id}").Get(user.GetPublicKey). + Delete(user.DeletePublicKey) + }) + + // (admin:application scope) + m.Group("/applications", func() { + m.Combo("/oauth2"). + Get(user.ListOauth2Applications). + Post(bind(api.CreateOAuth2ApplicationOptions{}), user.CreateOauth2Application) + m.Combo("/oauth2/{id}"). + Delete(user.DeleteOauth2Application). + Patch(bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application). + Get(user.GetOauth2Application) + }) + + // (admin:gpg_key scope) + m.Group("/gpg_keys", func() { + m.Combo("").Get(user.ListMyGPGKeys). + Post(bind(api.CreateGPGKeyOption{}), user.CreateGPGKey) + m.Combo("/{id}").Get(user.GetGPGKey). + Delete(user.DeleteGPGKey) + }) + m.Get("/gpg_key_token", user.GetVerificationToken) + m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey) + + // (repo scope) + m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos). + Post(bind(api.CreateRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetUser), repo.Create) + + // (repo scope) + if !setting.Repository.DisableStars { + m.Group("/starred", func() { + m.Get("", user.GetMyStarredRepos) + m.Group("/{username}/{reponame}", func() { + m.Get("", user.IsStarring) + m.Put("", user.Star) + m.Delete("", user.Unstar) + }, repoAssignment(), checkTokenPublicOnly()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) + } + m.Get("/times", repo.ListMyTrackedTimes) + m.Get("/stopwatches", repo.GetStopwatches) + m.Get("/subscriptions", user.GetMyWatchedRepos) + m.Get("/teams", org.ListUserTeams) + m.Group("/hooks", func() { + m.Combo("").Get(user.ListHooks). + Post(bind(api.CreateHookOption{}), user.CreateHook) + m.Combo("/{id}").Get(user.GetHook). + Patch(bind(api.EditHookOption{}), user.EditHook). + Delete(user.DeleteHook) + }, reqWebhooksEnabled()) + + m.Group("", func() { + m.Get("/list_blocked", user.ListBlockedUsers) + m.Group("", func() { + m.Put("/block/{username}", user.BlockUser) + m.Put("/unblock/{username}", user.UnblockUser) + }, context.UserAssignmentAPI()) + }) + + m.Group("/avatar", func() { + m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar) + m.Delete("", user.DeleteAvatar) + }, reqToken()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) + + // Repositories (requires repo scope, org scope) + m.Post("/org/{org}/repos", + // FIXME: we need org in context + tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository), + reqToken(), + bind(api.CreateRepoOption{}), + repo.CreateOrgRepoDeprecated) + + // requires repo scope + // FIXME: Don't expose repository id outside of the system + m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(repo.GetByID) + + // Repos (requires repo scope) + m.Group("/repos", func() { + m.Get("/search", repo.Search) + + // (repo scope) + m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate) + + m.Group("/{username}/{reponame}", func() { + m.Get("/compare/*", reqRepoReader(unit.TypeCode), repo.CompareDiff) + + m.Combo("").Get(reqAnyRepoReader(), repo.Get). + Delete(reqToken(), reqOwner(), repo.Delete). + Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit) + m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate) + m.Group("/transfer", func() { + m.Post("", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer) + m.Post("/accept", repo.AcceptTransfer) + m.Post("/reject", repo.RejectTransfer) + }, reqToken()) + addActionsRoutes( + m, + reqOwner(), + repo.NewAction(), + ) + m.Group("/hooks/git", func() { + m.Combo("").Get(repo.ListGitHooks) + m.Group("/{id}", func() { + m.Combo("").Get(repo.GetGitHook). + Patch(bind(api.EditGitHookOption{}), repo.EditGitHook). + Delete(repo.DeleteGitHook) + }) + }, reqToken(), reqAdmin(), reqGitHook(), context.ReferencesGitRepo(true)) + m.Group("/hooks", func() { + m.Combo("").Get(repo.ListHooks). + Post(bind(api.CreateHookOption{}), repo.CreateHook) + m.Group("/{id}", func() { + m.Combo("").Get(repo.GetHook). + Patch(bind(api.EditHookOption{}), repo.EditHook). + Delete(repo.DeleteHook) + m.Post("/tests", context.ReferencesGitRepo(), context.RepoRefForAPI, repo.TestHook) + }) + }, reqToken(), reqAdmin(), reqWebhooksEnabled()) + m.Group("/collaborators", func() { + m.Get("", reqAnyRepoReader(), repo.ListCollaborators) + m.Group("/{collaborator}", func() { + m.Combo("").Get(reqAnyRepoReader(), repo.IsCollaborator). + Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddCollaborator). + Delete(reqAdmin(), repo.DeleteCollaborator) + m.Get("/permission", repo.GetRepoPermissions) + }) + }, reqToken()) + if setting.Repository.EnableFlags { + m.Group("/flags", func() { + m.Combo("").Get(repo.ListFlags). + Put(bind(api.ReplaceFlagsOption{}), repo.ReplaceAllFlags). + Delete(repo.DeleteAllFlags) + m.Group("/{flag}", func() { + m.Combo("").Get(repo.HasFlag). + Put(repo.AddFlag). + Delete(repo.DeleteFlag) + }) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin()) + } + m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees) + m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers) + m.Group("/teams", func() { + m.Get("", reqAnyRepoReader(), repo.ListTeams) + m.Combo("/{team}").Get(reqAnyRepoReader(), repo.IsTeam). + Put(reqAdmin(), repo.AddTeam). + Delete(reqAdmin(), repo.DeleteTeam) + }, reqToken()) + m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile) + m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS) + m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) + if !setting.Repository.DisableForks { + m.Combo("/forks").Get(repo.ListForks). + Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) + } + m.Group("/branches", func() { + m.Get("", repo.ListBranches) + m.Get("/*", repo.GetBranch) + m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch) + m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.CreateBranch) + }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) + m.Group("/branch_protections", func() { + m.Get("", repo.ListBranchProtections) + m.Post("", bind(api.CreateBranchProtectionOption{}), mustNotBeArchived, repo.CreateBranchProtection) + m.Group("/{name}", func() { + m.Get("", repo.GetBranchProtection) + m.Patch("", bind(api.EditBranchProtectionOption{}), mustNotBeArchived, repo.EditBranchProtection) + m.Delete("", repo.DeleteBranchProtection) + }) + }, reqToken(), reqAdmin()) + m.Group("/tags", func() { + m.Get("", repo.ListTags) + m.Get("/*", repo.GetTag) + m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.CreateTag) + m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag) + }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true)) + m.Group("/tag_protections", func() { + m.Combo("").Get(repo.ListTagProtection). + Post(bind(api.CreateTagProtectionOption{}), mustNotBeArchived, repo.CreateTagProtection) + m.Group("/{id}", func() { + m.Combo("").Get(repo.GetTagProtection). + Patch(bind(api.EditTagProtectionOption{}), mustNotBeArchived, repo.EditTagProtection). + Delete(repo.DeleteTagProtection) + }) + }, reqToken(), reqAdmin()) + m.Group("/actions", func() { + m.Get("/tasks", repo.ListActionTasks) + + m.Group("/workflows", func() { + m.Group("/{workflowname}", func() { + m.Post("/dispatches", reqToken(), reqRepoWriter(unit.TypeActions), mustNotBeArchived, bind(api.DispatchWorkflowOption{}), repo.DispatchWorkflow) + }) + }) + }, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true)) + m.Group("/keys", func() { + m.Combo("").Get(repo.ListDeployKeys). + Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey) + m.Combo("/{id}").Get(repo.GetDeployKey). + Delete(repo.DeleteDeploykey) + }, reqToken(), reqAdmin()) + m.Group("/times", func() { + m.Combo("").Get(repo.ListTrackedTimesByRepository) + m.Combo("/{timetrackingusername}").Get(repo.ListTrackedTimesByUser) + }, mustEnableIssues, reqToken()) + m.Group("/wiki", func() { + m.Combo("/page/{pageName}"). + Get(repo.GetWikiPage). + Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeWiki, context.QuotaTargetRepo), repo.EditWikiPage). + Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage) + m.Get("/revisions/{pageName}", repo.ListPageRevisions) + m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeWiki, context.QuotaTargetRepo), repo.NewWikiPage) + m.Get("/pages", repo.ListWikiPages) + }, mustEnableWiki) + m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) + m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown) + m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw) + if !setting.Repository.DisableStars { + m.Get("/stargazers", repo.ListStargazers) + } + m.Get("/subscribers", repo.ListSubscribers) + m.Group("/subscription", func() { + m.Get("", user.IsWatching) + m.Put("", user.Watch) + m.Delete("", user.Unwatch) + }, reqToken()) + m.Group("/releases", func() { + m.Combo("").Get(repo.ListReleases). + Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.CreateReleaseOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.CreateRelease) + m.Combo("/latest").Get(repo.GetLatestRelease) + m.Group("/{id}", func() { + m.Combo("").Get(repo.GetRelease). + Patch(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.EditReleaseOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.EditRelease). + Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease) + m.Group("/assets", func() { + m.Combo("").Get(repo.ListReleaseAttachments). + Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsReleases, context.QuotaTargetRepo), repo.CreateReleaseAttachment) + m.Combo("/{attachment_id}").Get(repo.GetReleaseAttachment). + Patch(reqToken(), reqRepoWriter(unit.TypeReleases), bind(api.EditAttachmentOptions{}), repo.EditReleaseAttachment). + Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseAttachment) + }) + }) + m.Group("/tags", func() { + m.Combo("/{tag}"). + Get(repo.GetReleaseByTag). + Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag) + }) + }, reqRepoReader(unit.TypeReleases)) + m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.MirrorSync) + m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), mustNotBeArchived, repo.PushMirrorSync) + m.Group("/push_mirrors", func() { + m.Combo("").Get(repo.ListPushMirrors). + Post(mustNotBeArchived, bind(api.CreatePushMirrorOption{}), repo.AddPushMirror) + m.Combo("/{name}"). + Delete(mustNotBeArchived, repo.DeletePushMirrorByRemoteName). + Get(repo.GetPushMirrorByName) + }, reqAdmin(), reqToken()) + + m.Get("/editorconfig/{filename}", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetEditorconfig) + m.Group("/pulls", func() { + m.Combo("").Get(repo.ListPullRequests). + Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest) + m.Get("/pinned", repo.ListPinnedPullRequests) + m.Group("/{index}", func() { + m.Combo("").Get(repo.GetPullRequest). + Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest) + m.Get(".{diffType:diff|patch}", repo.DownloadPullDiffOrPatch) + m.Post("/update", reqToken(), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.UpdatePullRequest) + m.Get("/commits", repo.GetPullRequestCommits) + m.Get("/files", repo.GetPullRequestFiles) + m.Combo("/merge").Get(repo.IsPullRequestMerged). + Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.MergePullRequest). + Delete(reqToken(), mustNotBeArchived, repo.CancelScheduledAutoMerge) + m.Group("/reviews", func() { + m.Combo(""). + Get(repo.ListPullReviews). + Post(reqToken(), bind(api.CreatePullReviewOptions{}), repo.CreatePullReview) + m.Group("/{id}", func() { + m.Combo(""). + Get(repo.GetPullReview). + Delete(reqToken(), repo.DeletePullReview). + Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview) + m.Group("/comments", func() { + m.Combo(""). + Get(repo.GetPullReviewComments). + Post(reqToken(), bind(api.CreatePullReviewCommentOptions{}), repo.CreatePullReviewComment) + m.Group("/{comment}", func() { + m.Combo(""). + Get(repo.GetPullReviewComment). + Delete(reqToken(), repo.DeletePullReviewComment) + }, commentAssignment("comment")) + }) + m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview) + m.Post("/undismissals", reqToken(), repo.UnDismissPullReview) + }) + }) + m.Combo("/requested_reviewers", reqToken()). + Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests). + Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests) + }) + m.Get("/{base}/*", repo.GetPullRequestByBaseHead) + }, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()) + m.Group("/statuses", func() { + m.Combo("/{sha}").Get(repo.GetCommitStatuses). + Post(reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateStatusOption{}), repo.NewCommitStatus) + }, reqRepoReader(unit.TypeCode)) + m.Group("/commits", func() { + m.Get("", context.ReferencesGitRepo(), repo.GetAllCommits) + m.Group("/{ref}", func() { + m.Get("/status", repo.GetCombinedCommitStatusByRef) + m.Get("/statuses", repo.GetCommitStatusesByRef) + m.Get("/pull", repo.GetCommitPullRequest) + }, context.ReferencesGitRepo()) + }, reqRepoReader(unit.TypeCode)) + m.Group("/git", func() { + m.Group("/commits", func() { + m.Get("/{sha}", repo.GetSingleCommit) + m.Get("/{sha}.{diffType:diff|patch}", repo.DownloadCommitDiffOrPatch) + }) + m.Get("/refs", repo.GetGitAllRefs) + m.Get("/refs/*", repo.GetGitRefs) + m.Get("/trees/{sha}", repo.GetTree) + m.Get("/blobs/{sha}", repo.GetBlob) + m.Get("/tags/{sha}", repo.GetAnnotatedTag) + m.Get("/notes/{sha}", repo.GetNote) + }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) + m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.ApplyDiffPatch) + m.Group("/contents", func() { + m.Get("", repo.GetContentsList) + m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.ChangeFiles) + m.Get("/*", repo.GetContents) + m.Group("/*", func() { + m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.CreateFile) + m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.UpdateFile) + m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.DeleteFile) + }, reqToken()) + }, reqRepoReader(unit.TypeCode)) + m.Get("/signing-key.gpg", misc.SigningKey) + m.Group("/topics", func() { + m.Combo("").Get(repo.ListTopics). + Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics) + m.Group("/{topic}", func() { + m.Combo("").Put(reqToken(), repo.AddTopic). + Delete(reqToken(), repo.DeleteTopic) + }, reqAdmin()) + }, reqAnyRepoReader()) + m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates) + m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig) + m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig) + m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages) + m.Get("/activities/feeds", repo.ListRepoActivityFeeds) + m.Get("/new_pin_allowed", repo.AreNewIssuePinsAllowed) + m.Group("/avatar", func() { + m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar) + m.Delete("", repo.DeleteAvatar) + }, reqAdmin(), reqToken()) + }, repoAssignment(), checkTokenPublicOnly()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) + + // Notifications (requires notifications scope) + m.Group("/repos", func() { + m.Group("/{username}/{reponame}", func() { + m.Combo("/notifications", reqToken()). + Get(notify.ListRepoNotifications). + Put(notify.ReadRepoNotifications) + }, repoAssignment(), checkTokenPublicOnly()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification)) + + // Issue (requires issue scope) + m.Group("/repos", func() { + m.Get("/issues/search", repo.SearchIssues) + + m.Group("/{username}/{reponame}", func() { + m.Group("/issues", func() { + m.Combo("").Get(repo.ListIssues). + Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue) + m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues) + m.Group("/comments", func() { + m.Get("", repo.ListRepoIssueComments) + m.Group("/{id}", func() { + m.Combo(""). + Get(repo.GetIssueComment). + Patch(mustNotBeArchived, reqToken(), bind(api.EditIssueCommentOption{}), repo.EditIssueComment). + Delete(reqToken(), repo.DeleteIssueComment) + m.Combo("/reactions"). + Get(repo.GetIssueCommentReactions). + Post(reqToken(), bind(api.EditReactionOption{}), repo.PostIssueCommentReaction). + Delete(reqToken(), bind(api.EditReactionOption{}), repo.DeleteIssueCommentReaction) + m.Group("/assets", func() { + m.Combo(""). + Get(repo.ListIssueCommentAttachments). + Post(reqToken(), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsIssues, context.QuotaTargetRepo), repo.CreateIssueCommentAttachment) + m.Combo("/{attachment_id}"). + Get(repo.GetIssueCommentAttachment). + Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment). + Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueCommentAttachment) + }, mustEnableAttachments) + }, commentAssignment(":id")) + }) + m.Group("/{index}", func() { + m.Combo("").Get(repo.GetIssue). + Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue). + Delete(reqToken(), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue) + m.Group("/comments", func() { + m.Combo("").Get(repo.ListIssueComments). + Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment) + m.Combo("/{id}", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated). + Delete(repo.DeleteIssueCommentDeprecated) + }) + m.Get("/timeline", repo.ListIssueCommentsAndTimeline) + m.Group("/labels", func() { + m.Combo("").Get(repo.ListIssueLabels). + Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels). + Put(reqToken(), bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels). + Delete(reqToken(), bind(api.DeleteLabelsOption{}), repo.ClearIssueLabels) + m.Delete("/{id}", reqToken(), bind(api.DeleteLabelsOption{}), repo.DeleteIssueLabel) + }) + m.Group("/times", func() { + m.Combo(""). + Get(repo.ListTrackedTimes). + Post(bind(api.AddTimeOption{}), repo.AddTime). + Delete(repo.ResetIssueTime) + m.Delete("/{id}", repo.DeleteTime) + }, reqToken()) + m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline) + m.Group("/stopwatch", func() { + m.Post("/start", repo.StartIssueStopwatch) + m.Post("/stop", repo.StopIssueStopwatch) + m.Delete("/delete", repo.DeleteIssueStopwatch) + }, reqToken()) + m.Group("/subscriptions", func() { + m.Get("", repo.GetIssueSubscribers) + m.Get("/check", reqToken(), repo.CheckIssueSubscription) + m.Put("/{user}", reqToken(), repo.AddIssueSubscription) + m.Delete("/{user}", reqToken(), repo.DelIssueSubscription) + }) + m.Combo("/reactions"). + Get(repo.GetIssueReactions). + Post(reqToken(), bind(api.EditReactionOption{}), repo.PostIssueReaction). + Delete(reqToken(), bind(api.EditReactionOption{}), repo.DeleteIssueReaction) + m.Group("/assets", func() { + m.Combo(""). + Get(repo.ListIssueAttachments). + Post(reqToken(), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsIssues, context.QuotaTargetRepo), repo.CreateIssueAttachment) + m.Combo("/{attachment_id}"). + Get(repo.GetIssueAttachment). + Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment). + Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueAttachment) + }, mustEnableAttachments) + m.Combo("/dependencies"). + Get(repo.GetIssueDependencies). + Post(reqToken(), mustNotBeArchived, bind(api.IssueMeta{}), repo.CreateIssueDependency). + Delete(reqToken(), mustNotBeArchived, bind(api.IssueMeta{}), repo.RemoveIssueDependency) + m.Combo("/blocks"). + Get(repo.GetIssueBlocks). + Post(reqToken(), bind(api.IssueMeta{}), repo.CreateIssueBlocking). + Delete(reqToken(), bind(api.IssueMeta{}), repo.RemoveIssueBlocking) + m.Group("/pin", func() { + m.Combo(""). + Post(reqToken(), reqAdmin(), repo.PinIssue). + Delete(reqToken(), reqAdmin(), repo.UnpinIssue) + m.Patch("/{position}", reqToken(), reqAdmin(), repo.MoveIssuePin) + }) + }) + }, mustEnableIssuesOrPulls) + m.Group("/labels", func() { + m.Combo("").Get(repo.ListLabels). + Post(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateLabelOption{}), repo.CreateLabel) + m.Combo("/{id}").Get(repo.GetLabel). + Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditLabelOption{}), repo.EditLabel). + Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteLabel) + }) + m.Group("/milestones", func() { + m.Combo("").Get(repo.ListMilestones). + Post(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateMilestoneOption{}), repo.CreateMilestone) + m.Combo("/{id}").Get(repo.GetMilestone). + Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone). + Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone) + }) + }, repoAssignment(), checkTokenPublicOnly()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue)) + + // NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs + m.Group("/packages/{username}", func() { + m.Group("/{type}/{name}/{version}", func() { + m.Get("", reqToken(), packages.GetPackage) + m.Delete("", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) + m.Get("/files", reqToken(), packages.ListPackageFiles) + }) + m.Get("/", reqToken(), packages.ListPackages) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly()) + + // Organizations + m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs) + m.Group("/users/{username}/orgs", func() { + m.Get("", reqToken(), org.ListUserOrgs) + m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly()) + m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create) + m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization)) + m.Group("/orgs/{org}", func() { + m.Combo("").Get(org.Get). + Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). + Delete(reqToken(), reqOrgOwnership(), org.Delete) + m.Combo("/repos").Get(user.ListOrgRepos). + Post(reqToken(), bind(api.CreateRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetOrg), repo.CreateOrgRepo) + m.Group("/members", func() { + m.Get("", reqToken(), org.ListMembers) + m.Combo("/{username}").Get(reqToken(), org.IsMember). + Delete(reqToken(), reqOrgOwnership(), org.DeleteMember) + }) + addActionsRoutes( + m, + reqOrgOwnership(), + org.NewAction(), + ) + m.Group("/public_members", func() { + m.Get("", org.ListPublicMembers) + m.Combo("/{username}").Get(org.IsPublicMember). + Put(reqToken(), reqOrgMembership(), org.PublicizeMember). + Delete(reqToken(), reqOrgMembership(), org.ConcealMember) + }) + m.Group("/teams", func() { + m.Get("", org.ListTeams) + m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam) + m.Get("/search", org.SearchTeam) + }, reqToken(), reqOrgMembership()) + m.Group("/labels", func() { + m.Get("", org.ListLabels) + m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel) + m.Combo("/{id}").Get(reqToken(), org.GetLabel). + Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel). + Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel) + }) + m.Group("/hooks", func() { + m.Combo("").Get(org.ListHooks). + Post(bind(api.CreateHookOption{}), org.CreateHook) + m.Combo("/{id}").Get(org.GetHook). + Patch(bind(api.EditHookOption{}), org.EditHook). + Delete(org.DeleteHook) + }, reqToken(), reqOrgOwnership(), reqWebhooksEnabled()) + m.Group("/avatar", func() { + m.Post("", bind(api.UpdateUserAvatarOption{}), org.UpdateAvatar) + m.Delete("", org.DeleteAvatar) + }, reqToken(), reqOrgOwnership()) + m.Get("/activities/feeds", org.ListOrgActivityFeeds) + + if setting.Quota.Enabled { + m.Group("/quota", func() { + m.Get("", org.GetQuota) + m.Get("/check", org.CheckQuota) + m.Get("/attachments", org.ListQuotaAttachments) + m.Get("/packages", org.ListQuotaPackages) + m.Get("/artifacts", org.ListQuotaArtifacts) + }, reqToken(), reqOrgOwnership()) + } + + m.Group("", func() { + m.Get("/list_blocked", org.ListBlockedUsers) + m.Group("", func() { + m.Put("/block/{username}", org.BlockUser) + m.Put("/unblock/{username}", org.UnblockUser) + }, context.UserAssignmentAPI()) + }, reqToken(), reqOrgOwnership()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly()) + m.Group("/teams/{teamid}", func() { + m.Combo("").Get(reqToken(), org.GetTeam). + Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam). + Delete(reqToken(), reqOrgOwnership(), org.DeleteTeam) + m.Group("/members", func() { + m.Get("", reqToken(), org.GetTeamMembers) + m.Combo("/{username}"). + Get(reqToken(), org.GetTeamMember). + Put(reqToken(), reqOrgOwnership(), org.AddTeamMember). + Delete(reqToken(), reqOrgOwnership(), org.RemoveTeamMember) + }) + m.Group("/repos", func() { + m.Get("", reqToken(), org.GetTeamRepos) + m.Combo("/{org}/{reponame}"). + Put(reqToken(), org.AddTeamRepository). + Delete(reqToken(), org.RemoveTeamRepository). + Get(reqToken(), org.GetTeamRepo) + }) + m.Get("/activities/feeds", org.ListTeamActivityFeeds) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership(), checkTokenPublicOnly()) + + m.Group("/admin", func() { + m.Group("/cron", func() { + m.Get("", admin.ListCronTasks) + m.Post("/{task}", admin.PostCronTask) + }) + m.Get("/orgs", admin.GetAllOrgs) + m.Group("/users", func() { + m.Get("", admin.SearchUsers) + m.Post("", bind(api.CreateUserOption{}), admin.CreateUser) + m.Group("/{username}", func() { + m.Combo("").Patch(bind(api.EditUserOption{}), admin.EditUser). + Delete(admin.DeleteUser) + m.Group("/keys", func() { + m.Post("", bind(api.CreateKeyOption{}), admin.CreatePublicKey) + m.Delete("/{id}", admin.DeleteUserPublicKey) + }) + m.Get("/orgs", org.ListUserOrgs) + m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg) + m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo) + m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser) + if setting.Quota.Enabled { + m.Group("/quota", func() { + m.Get("", admin.GetUserQuota) + m.Post("/groups", bind(api.SetUserQuotaGroupsOptions{}), admin.SetUserQuotaGroups) + }) + } + }, context.UserAssignmentAPI()) + }) + m.Group("/emails", func() { + m.Get("", admin.GetAllEmails) + m.Get("/search", admin.SearchEmail) + }) + m.Group("/unadopted", func() { + m.Get("", admin.ListUnadoptedRepositories) + m.Post("/{username}/{reponame}", admin.AdoptRepository) + m.Delete("/{username}/{reponame}", admin.DeleteUnadoptedRepository) + }) + m.Group("/hooks", func() { + m.Combo("").Get(admin.ListHooks). + Post(bind(api.CreateHookOption{}), admin.CreateHook) + m.Combo("/{id}").Get(admin.GetHook). + Patch(bind(api.EditHookOption{}), admin.EditHook). + Delete(admin.DeleteHook) + }) + m.Group("/runners", func() { + m.Get("/registration-token", admin.GetRegistrationToken) + }) + if setting.Quota.Enabled { + m.Group("/quota", func() { + m.Group("/rules", func() { + m.Combo("").Get(admin.ListQuotaRules). + Post(bind(api.CreateQuotaRuleOptions{}), admin.CreateQuotaRule) + m.Combo("/{quotarule}", context.QuotaRuleAssignmentAPI()). + Get(admin.GetQuotaRule). + Patch(bind(api.EditQuotaRuleOptions{}), admin.EditQuotaRule). + Delete(admin.DeleteQuotaRule) + }) + m.Group("/groups", func() { + m.Combo("").Get(admin.ListQuotaGroups). + Post(bind(api.CreateQuotaGroupOptions{}), admin.CreateQuotaGroup) + m.Group("/{quotagroup}", func() { + m.Combo("").Get(admin.GetQuotaGroup). + Delete(admin.DeleteQuotaGroup) + m.Group("/rules", func() { + m.Combo("/{quotarule}", context.QuotaRuleAssignmentAPI()). + Put(admin.AddRuleToQuotaGroup). + Delete(admin.RemoveRuleFromQuotaGroup) + }) + m.Group("/users", func() { + m.Get("", admin.ListUsersInQuotaGroup) + m.Combo("/{username}", context.UserAssignmentAPI()). + Put(admin.AddUserToQuotaGroup). + Delete(admin.RemoveUserFromQuotaGroup) + }) + }, context.QuotaGroupAssignmentAPI()) + }) + }) + } + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin()) + + m.Group("/topics", func() { + m.Get("/search", repo.TopicSearch) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) + }, sudo()) + + return m +} |