mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-07-25 02:48:05 +00:00
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/13143 The details of the refactor are in [a separate branch](https://codeberg.org/limiting-factor/forgejo/commits/branch/wip-api-context-accessors-save) which details how it was done. This pull request contains a squash of those commits rebased on top of the current Forgejo branch, which involved conflict resolution. Given the magnitude of the change, there is a high chance that a large number of future pull requests merged in Forgejo will create conflicts. They will be dealt with in the same way as chore: refactor orgAssignment to two separate middlewares (#13155). ## Review The only risk I can think of is a getter/setter implementation being incorrectly implemented. This is probably where a detailed review is most needed, in the `services/context/api.go` file. To not increase the size of the pull request, the `GetX()` accessors were preserved although they are redundant with `X()`. They will be removed in a followup pull request. ## Backporting strategy. Cherry-picking the commit to v15 has a significant amount of conflicts in 8 files out of 111. ``` unmerged routers/api/v1/activitypub/repository.go unmerged routers/api/v1/admin/user.go unmerged routers/api/v1/api.go unmerged routers/api/v1/org/team.go unmerged routers/api/v1/repo/action.go unmerged routers/api/v1/repo/pull.go unmerged routers/api/v1/user/follower.go unmerged services/context/api.go ``` Instead of resolving them, I think it is easier and safer to repeat the [refactor steps](https://codeberg.org/limiting-factor/forgejo/commits/branch/wip-api-context-accessors-save) in v15. I also think it is worth the effort since it will avoid trivial backport conflicts on over 100 files. ## Context This is a followup pull request discussed originally at https://codeberg.org/forgejo/forgejo/pulls/12512 Co-authored-by: limiting-factor <limiting-factor@posteo.com> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/13213 Reviewed-by: limiting-factor <limiting-factor@noreply.codeberg.org> Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
200 lines
5.4 KiB
Go
200 lines
5.4 KiB
Go
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package context
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
quota_model "forgejo.org/models/quota"
|
|
"forgejo.org/modules/base"
|
|
)
|
|
|
|
type QuotaTargetType int
|
|
|
|
const (
|
|
QuotaTargetUser QuotaTargetType = iota
|
|
QuotaTargetRepo
|
|
QuotaTargetOrg
|
|
)
|
|
|
|
// QuotaExceeded
|
|
// swagger:response quotaExceeded
|
|
type APIQuotaExceeded struct {
|
|
Message string `json:"message"`
|
|
UserID int64 `json:"user_id"`
|
|
UserName string `json:"username,omitempty"`
|
|
}
|
|
|
|
// QuotaGroupAssignmentAPI returns a middleware to handle context-quota-group assignment for api routes
|
|
func QuotaGroupAssignmentAPI() func(ctx *APIContext) {
|
|
return func(ctx *APIContext) {
|
|
groupName := ctx.Params("quotagroup")
|
|
group, err := quota_model.GetGroupByName(ctx, groupName)
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "quota_model.GetGroupByName", err)
|
|
return
|
|
}
|
|
if group == nil {
|
|
ctx.NotFound()
|
|
return
|
|
}
|
|
ctx.quotaGroup = group
|
|
}
|
|
}
|
|
|
|
// QuotaRuleAssignmentAPI returns a middleware to handle context-quota-rule assignment for api routes
|
|
func QuotaRuleAssignmentAPI() func(ctx *APIContext) {
|
|
return func(ctx *APIContext) {
|
|
ruleName := ctx.Params("quotarule")
|
|
rule, err := quota_model.GetRuleByName(ctx, ruleName)
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "quota_model.GetRuleByName", err)
|
|
return
|
|
}
|
|
if rule == nil {
|
|
ctx.NotFound()
|
|
return
|
|
}
|
|
ctx.quotaRule = rule
|
|
}
|
|
}
|
|
|
|
// ctx.CheckQuota checks whether the user in question is within quota limits (web context)
|
|
func (ctx *Context) CheckQuota(subject quota_model.LimitSubject, userID int64, username string) bool {
|
|
ok, err := checkQuota(ctx.originCtx, subject, userID, username, func(userID int64, username string) {
|
|
showHTML := false
|
|
for _, part := range ctx.Req.Header["Accept"] {
|
|
if strings.Contains(part, "text/html") {
|
|
showHTML = true
|
|
break
|
|
}
|
|
}
|
|
if !showHTML {
|
|
ctx.plainTextInternal(3, http.StatusRequestEntityTooLarge, []byte("Quota exceeded.\n"))
|
|
return
|
|
}
|
|
|
|
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
|
ctx.Data["Title"] = "Quota Exceeded"
|
|
ctx.HTML(http.StatusRequestEntityTooLarge, base.TplName("status/413"))
|
|
}, func(err error) {
|
|
ctx.Error(http.StatusInternalServerError, "quota_model.EvaluateForUser")
|
|
})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return ok
|
|
}
|
|
|
|
// ctx.CheckQuota checks whether the user in question is within quota limits (API context)
|
|
func (ctx *APIContext) CheckQuota(subject quota_model.LimitSubject, userID int64, username string) bool {
|
|
ok, err := checkQuota(ctx.originCtx, subject, userID, username, func(userID int64, username string) {
|
|
ctx.JSON(http.StatusRequestEntityTooLarge, APIQuotaExceeded{
|
|
Message: "quota exceeded",
|
|
UserID: userID,
|
|
UserName: username,
|
|
})
|
|
}, func(err error) {
|
|
ctx.InternalServerError(err)
|
|
})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return ok
|
|
}
|
|
|
|
// EnforceQuotaWeb returns a middleware that enforces quota limits on the given web route.
|
|
func EnforceQuotaWeb(subject quota_model.LimitSubject, target QuotaTargetType) func(ctx *Context) {
|
|
return func(ctx *Context) {
|
|
ctx.CheckQuota(subject, target.UserID(ctx), target.UserName(ctx))
|
|
}
|
|
}
|
|
|
|
// EnforceQuotaWeb returns a middleware that enforces quota limits on the given API route.
|
|
func EnforceQuotaAPI(subject quota_model.LimitSubject, target QuotaTargetType) func(ctx *APIContext) {
|
|
return func(ctx *APIContext) {
|
|
ctx.CheckQuota(subject, target.UserID(ctx), target.UserName(ctx))
|
|
}
|
|
}
|
|
|
|
// checkQuota wraps quota checking into a single function
|
|
func checkQuota(ctx context.Context, subject quota_model.LimitSubject, userID int64, username string, quotaExceededHandler func(userID int64, username string), errorHandler func(err error)) (bool, error) {
|
|
ok, err := quota_model.EvaluateForUser(ctx, userID, subject)
|
|
if err != nil {
|
|
errorHandler(err)
|
|
return false, err
|
|
}
|
|
if !ok {
|
|
quotaExceededHandler(userID, username)
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
type QuotaContext interface {
|
|
GetQuotaTargetUserID(target QuotaTargetType) int64
|
|
GetQuotaTargetUserName(target QuotaTargetType) string
|
|
}
|
|
|
|
func (ctx *Context) GetQuotaTargetUserID(target QuotaTargetType) int64 {
|
|
switch target {
|
|
case QuotaTargetUser:
|
|
return ctx.Doer.ID
|
|
case QuotaTargetRepo:
|
|
return ctx.Repo.Repository.OwnerID
|
|
case QuotaTargetOrg:
|
|
return ctx.Org.Organization.ID
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func (ctx *Context) GetQuotaTargetUserName(target QuotaTargetType) string {
|
|
switch target {
|
|
case QuotaTargetUser:
|
|
return ctx.Doer.Name
|
|
case QuotaTargetRepo:
|
|
return ctx.Repo.Repository.Owner.Name
|
|
case QuotaTargetOrg:
|
|
return ctx.Org.Organization.Name
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (ctx *APIContext) GetQuotaTargetUserID(target QuotaTargetType) int64 {
|
|
switch target {
|
|
case QuotaTargetUser:
|
|
return ctx.Doer().ID
|
|
case QuotaTargetRepo:
|
|
return ctx.Repo().Repository.OwnerID
|
|
case QuotaTargetOrg:
|
|
return ctx.Org().Organization.ID
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func (ctx *APIContext) GetQuotaTargetUserName(target QuotaTargetType) string {
|
|
switch target {
|
|
case QuotaTargetUser:
|
|
return ctx.Doer().Name
|
|
case QuotaTargetRepo:
|
|
return ctx.Repo().Repository.Owner.Name
|
|
case QuotaTargetOrg:
|
|
return ctx.Org().Organization.Name
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (target QuotaTargetType) UserID(ctx QuotaContext) int64 {
|
|
return ctx.GetQuotaTargetUserID(target)
|
|
}
|
|
|
|
func (target QuotaTargetType) UserName(ctx QuotaContext) string {
|
|
return ctx.GetQuotaTargetUserName(target)
|
|
}
|