gitforge/services/context/quota.go
forgejo-backport-action a5a2cae7ee [v16.0/forgejo] chore(refactor): add getter/setter to APIContext data members (#13213)
**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>
2026-06-27 18:27:57 +02:00

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)
}