fix: enforce package quota against package owner, not uploader (#11442)

## What is broken

Quota on packages is not enforced when pushing to an organisation.

`enforcePackagesQuota()` calls `EvaluateForUser(ctx.Doer.ID, ...)` — it checks how much space the **uploader** personally owns, not the org being pushed to. Since packages accumulate under `package.owner_id = org_id`, the uploader always shows 0 bytes used and the check always passes.

This also means site admins bypass quota entirely when pushing to orgs (they get the service-layer admin bypass on top of the 0-byte measurement).

OCI/container routes (`/v2/...`) have the same problem but worse — `enforcePackagesQuota()` was not called on them at all.

## Fix

Check quota against `ctx.Package.Owner.ID` instead of `ctx.Doer.ID`. The package owner (the org or user being pushed to) is already available via `ctx.Package.Owner`, populated by `PackageAssignment()` before this middleware runs.

For individual user namespaces nothing changes — `ctx.Package.Owner` is the user themselves.

Also wired `enforcePackagesQuota()` into the missing OCI upload routes: `InitiateUploadBlob`, `UploadBlob`, `EndUploadBlob`, `UploadManifest` — both in the named `/{image}` group and the wildcard `/*` handler.

## Tested

Kind cluster, org `maw2` with 1 GiB quota, 2.6 GiB of container images already pushed:

- pushing a generic package to `maw2` as SA user → was 201, now 413
- pushing a generic package to `maw2` as `gitea_admin` → was 201, now 413
- initiating OCI blob upload to `maw2` as SA user → was 202, now 413
- pushing to own user namespace within quota → still 201

Co-authored-by: azhluwi <lukasz.widera@convotis.ch>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11442
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Co-authored-by: wejdross <wejdross@noreply.codeberg.org>
Co-committed-by: wejdross <wejdross@noreply.codeberg.org>
This commit is contained in:
wejdross 2026-03-09 17:14:50 +01:00 committed by Mathieu Fenniak
parent 3934e5fea3
commit cf51d3c888
2 changed files with 190 additions and 6 deletions

View file

@ -94,7 +94,14 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
func enforcePackagesQuota() func(ctx *context.Context) {
return func(ctx *context.Context) {
ok, err := quota_model.EvaluateForUser(ctx, ctx.Doer.ID, quota_model.LimitSubjectSizeAssetsPackagesAll)
// Evaluate quota against the package owner (org or user the package is pushed to),
// not the uploader (ctx.Doer). This enables org-level quota: all members uploading
// to an org consume from the org's quota group, not their own personal quota.
ownerID := ctx.Doer.ID
if ctx.Package != nil {
ownerID = ctx.Package.Owner.ID
}
ok, err := quota_model.EvaluateForUser(ctx, ownerID, quota_model.LimitSubjectSizeAssetsPackagesAll)
if err != nil {
log.Error("quota_model.EvaluateForUser: %v", err)
ctx.Error(http.StatusInternalServerError, "Error checking quota")
@ -793,11 +800,11 @@ func ContainerRoutes() *web.Route {
r.Group("/{username}", func() {
r.Group("/{image}", func() {
r.Group("/blobs/uploads", func() {
r.Post("", container.InitiateUploadBlob)
r.Post("", enforcePackagesQuota(), container.InitiateUploadBlob)
r.Group("/{uuid}", func() {
r.Get("", container.GetUploadBlob)
r.Patch("", container.UploadBlob)
r.Put("", container.EndUploadBlob)
r.Patch("", enforcePackagesQuota(), container.UploadBlob)
r.Put("", enforcePackagesQuota(), container.EndUploadBlob)
r.Delete("", container.CancelUploadBlob)
})
}, reqPackageAccess(perm.AccessModeWrite))
@ -807,7 +814,7 @@ func ContainerRoutes() *web.Route {
r.Delete("", reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob)
})
r.Group("/manifests/{reference}", func() {
r.Put("", reqPackageAccess(perm.AccessModeWrite), container.UploadManifest)
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), container.UploadManifest)
r.Head("", container.HeadManifest)
r.Get("", container.GetManifest)
r.Delete("", reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest)
@ -843,6 +850,10 @@ func ContainerRoutes() *web.Route {
return
}
enforcePackagesQuota()(ctx)
if ctx.Written() {
return
}
container.InitiateUploadBlob(ctx)
return
}
@ -875,8 +886,16 @@ func ContainerRoutes() *web.Route {
if isGet {
container.GetUploadBlob(ctx)
} else if isPatch {
enforcePackagesQuota()(ctx)
if ctx.Written() {
return
}
container.UploadBlob(ctx)
} else if isPut {
enforcePackagesQuota()(ctx)
if ctx.Written() {
return
}
container.EndUploadBlob(ctx)
} else {
container.CancelUploadBlob(ctx)
@ -926,6 +945,10 @@ func ContainerRoutes() *web.Route {
return
}
if isPut {
enforcePackagesQuota()(ctx)
if ctx.Written() {
return
}
container.UploadManifest(ctx)
} else {
container.DeleteManifest(ctx)