feat(quota): Quota enforcement

The previous commit laid out the foundation of the quota engine, this
one builds on top of it, and implements the actual enforcement.

Enforcement happens at the route decoration level, whenever possible. In
case of the API, when over quota, a 413 error is returned, with an
appropriate JSON payload. In case of web routes, a 413 HTML page is
rendered with similar information.

This implementation is for a **soft quota**: quota usage is checked
before an operation is to be performed, and the operation is *only*
denied if the user is already over quota. This makes it possible to go
over quota, but has the significant advantage of being practically
implementable within the current Forgejo architecture.

The goal of enforcement is to deny actions that can make the user go
over quota, and allow the rest. As such, deleting things should - in
almost all cases - be possible. A prime exemption is deleting files via
the web ui: that creates a new commit, which in turn increases repo
size, thus, is denied if the user is over quota.

Limitations
-----------

Because we generally work at a route decorator level, and rarely
look *into* the operation itself, `size:repos:public` and
`size:repos:private` are not enforced at this level, the engine enforces
against `size:repos:all`. This will be improved in the future.

AGit does not play very well with this system, because AGit PRs count
toward the repo they're opened against, while in the GitHub-style fork +
pull model, it counts against the fork. This too, can be improved in the
future.

There's very little done on the UI side to guard against going over
quota. What this patch implements, is enforcement, not prevention. The
UI will still let you *try* operations that *will* result in a denial.

Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
This commit is contained in:
Gergely Nagy 2024-07-06 10:30:16 +02:00
parent a414703c09
commit 67fa52dedb
No known key found for this signature in database
33 changed files with 3172 additions and 66 deletions

View file

@ -115,6 +115,7 @@ loading = Loading…
error = Error error = Error
error404 = The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it. error404 = The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it.
error413 = You have exhausted your quota.
go_back = Go Back go_back = Go Back
invalid_data = Invalid data: %v invalid_data = Invalid data: %v
@ -2196,6 +2197,7 @@ settings.units.add_more = Add more...
settings.sync_mirror = Synchronize now settings.sync_mirror = Synchronize now
settings.pull_mirror_sync_in_progress = Pulling changes from the remote %s at the moment. settings.pull_mirror_sync_in_progress = Pulling changes from the remote %s at the moment.
settings.pull_mirror_sync_quota_exceeded = Quota exceeded, not pulling changes.
settings.push_mirror_sync_in_progress = Pushing changes to the remote %s at the moment. settings.push_mirror_sync_in_progress = Pushing changes to the remote %s at the moment.
settings.site = Website settings.site = Website
settings.update_settings = Save settings settings.update_settings = Save settings
@ -2279,6 +2281,7 @@ settings.transfer_owner = New owner
settings.transfer_perform = Perform transfer settings.transfer_perform = Perform transfer
settings.transfer_started = This repository has been marked for transfer and awaits confirmation from "%s" settings.transfer_started = This repository has been marked for transfer and awaits confirmation from "%s"
settings.transfer_succeed = The repository has been transferred. settings.transfer_succeed = The repository has been transferred.
settings.transfer_quota_exceeded = The new owner (%s) is over quota. The repository has not been transferred.
settings.signing_settings = Signing verification settings settings.signing_settings = Signing verification settings
settings.trust_model = Signature trust model settings.trust_model = Signature trust model
settings.trust_model.default = Default trust model settings.trust_model.default = Default trust model

View file

@ -71,6 +71,7 @@ import (
"code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
quota_model "code.gitea.io/gitea/models/quota"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -240,6 +241,18 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
return return
} }
// check the owner's quota
ok, err := quota_model.EvaluateForUser(ctx, ctx.ActionTask.OwnerID, quota_model.LimitSubjectSizeAssetsArtifacts)
if err != nil {
log.Error("quota_model.EvaluateForUser: %v", err)
ctx.Error(http.StatusInternalServerError, "Error checking quota")
return
}
if !ok {
ctx.Error(http.StatusRequestEntityTooLarge, "Quota exceeded")
return
}
// get upload file size // get upload file size
fileRealTotalSize, contentLength := getUploadFileSize(ctx) fileRealTotalSize, contentLength := getUploadFileSize(ctx)

View file

@ -92,6 +92,7 @@ import (
"code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
quota_model "code.gitea.io/gitea/models/quota"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
@ -290,6 +291,18 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
return return
} }
// check the owner's quota
ok, err := quota_model.EvaluateForUser(ctx, task.OwnerID, quota_model.LimitSubjectSizeAssetsArtifacts)
if err != nil {
log.Error("quota_model.EvaluateForUser: %v", err)
ctx.Error(http.StatusInternalServerError, "Error checking quota")
return
}
if !ok {
ctx.Error(http.StatusRequestEntityTooLarge, "Quota exceeded")
return
}
comp := ctx.Req.URL.Query().Get("comp") comp := ctx.Req.URL.Query().Get("comp")
switch comp { switch comp {
case "block", "appendBlock": case "block", "appendBlock":

View file

@ -10,6 +10,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
quota_model "code.gitea.io/gitea/models/quota"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
@ -74,6 +75,21 @@ 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)
if err != nil {
log.Error("quota_model.EvaluateForUser: %v", err)
ctx.Error(http.StatusInternalServerError, "Error checking quota")
return
}
if !ok {
ctx.Error(http.StatusRequestEntityTooLarge, "enforcePackagesQuota", "quota exceeded")
return
}
}
}
func verifyAuth(r *web.Route, authMethods []auth.Method) { func verifyAuth(r *web.Route, authMethods []auth.Method) {
if setting.Service.EnableReverseProxyAuth { if setting.Service.EnableReverseProxyAuth {
authMethods = append(authMethods, &auth.ReverseProxy{}) authMethods = append(authMethods, &auth.ReverseProxy{})
@ -111,7 +127,7 @@ func CommonRoutes() *web.Route {
r.Group("/alpine", func() { r.Group("/alpine", func() {
r.Get("/key", alpine.GetRepositoryKey) r.Get("/key", alpine.GetRepositoryKey)
r.Group("/{branch}/{repository}", func() { r.Group("/{branch}/{repository}", func() {
r.Put("", reqPackageAccess(perm.AccessModeWrite), alpine.UploadPackageFile) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), alpine.UploadPackageFile)
r.Group("/{architecture}", func() { r.Group("/{architecture}", func() {
r.Get("/APKINDEX.tar.gz", alpine.GetRepositoryFile) r.Get("/APKINDEX.tar.gz", alpine.GetRepositoryFile)
r.Group("/{filename}", func() { r.Group("/{filename}", func() {
@ -124,12 +140,12 @@ func CommonRoutes() *web.Route {
r.Group("/cargo", func() { r.Group("/cargo", func() {
r.Group("/api/v1/crates", func() { r.Group("/api/v1/crates", func() {
r.Get("", cargo.SearchPackages) r.Get("", cargo.SearchPackages)
r.Put("/new", reqPackageAccess(perm.AccessModeWrite), cargo.UploadPackage) r.Put("/new", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), cargo.UploadPackage)
r.Group("/{package}", func() { r.Group("/{package}", func() {
r.Group("/{version}", func() { r.Group("/{version}", func() {
r.Get("/download", cargo.DownloadPackageFile) r.Get("/download", cargo.DownloadPackageFile)
r.Delete("/yank", reqPackageAccess(perm.AccessModeWrite), cargo.YankPackage) r.Delete("/yank", reqPackageAccess(perm.AccessModeWrite), cargo.YankPackage)
r.Put("/unyank", reqPackageAccess(perm.AccessModeWrite), cargo.UnyankPackage) r.Put("/unyank", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), cargo.UnyankPackage)
}) })
r.Get("/owners", cargo.ListOwners) r.Get("/owners", cargo.ListOwners)
}) })
@ -147,7 +163,7 @@ func CommonRoutes() *web.Route {
r.Get("/search", chef.EnumeratePackages) r.Get("/search", chef.EnumeratePackages)
r.Group("/cookbooks", func() { r.Group("/cookbooks", func() {
r.Get("", chef.EnumeratePackages) r.Get("", chef.EnumeratePackages)
r.Post("", reqPackageAccess(perm.AccessModeWrite), chef.UploadPackage) r.Post("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), chef.UploadPackage)
r.Group("/{name}", func() { r.Group("/{name}", func() {
r.Get("", chef.PackageMetadata) r.Get("", chef.PackageMetadata)
r.Group("/versions/{version}", func() { r.Group("/versions/{version}", func() {
@ -167,7 +183,7 @@ func CommonRoutes() *web.Route {
r.Get("/p2/{vendorname}/{projectname}~dev.json", composer.PackageMetadata) r.Get("/p2/{vendorname}/{projectname}~dev.json", composer.PackageMetadata)
r.Get("/p2/{vendorname}/{projectname}.json", composer.PackageMetadata) r.Get("/p2/{vendorname}/{projectname}.json", composer.PackageMetadata)
r.Get("/files/{package}/{version}/{filename}", composer.DownloadPackageFile) r.Get("/files/{package}/{version}/{filename}", composer.DownloadPackageFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), composer.UploadPackage) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), composer.UploadPackage)
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/conan", func() { r.Group("/conan", func() {
r.Group("/v1", func() { r.Group("/v1", func() {
@ -183,14 +199,14 @@ func CommonRoutes() *web.Route {
r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeleteRecipeV1) r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeleteRecipeV1)
r.Get("/search", conan.SearchPackagesV1) r.Get("/search", conan.SearchPackagesV1)
r.Get("/digest", conan.RecipeDownloadURLs) r.Get("/digest", conan.RecipeDownloadURLs)
r.Post("/upload_urls", reqPackageAccess(perm.AccessModeWrite), conan.RecipeUploadURLs) r.Post("/upload_urls", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.RecipeUploadURLs)
r.Get("/download_urls", conan.RecipeDownloadURLs) r.Get("/download_urls", conan.RecipeDownloadURLs)
r.Group("/packages", func() { r.Group("/packages", func() {
r.Post("/delete", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV1) r.Post("/delete", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV1)
r.Group("/{package_reference}", func() { r.Group("/{package_reference}", func() {
r.Get("", conan.PackageSnapshot) r.Get("", conan.PackageSnapshot)
r.Get("/digest", conan.PackageDownloadURLs) r.Get("/digest", conan.PackageDownloadURLs)
r.Post("/upload_urls", reqPackageAccess(perm.AccessModeWrite), conan.PackageUploadURLs) r.Post("/upload_urls", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.PackageUploadURLs)
r.Get("/download_urls", conan.PackageDownloadURLs) r.Get("/download_urls", conan.PackageDownloadURLs)
}) })
}) })
@ -199,11 +215,11 @@ func CommonRoutes() *web.Route {
r.Group("/files/{name}/{version}/{user}/{channel}/{recipe_revision}", func() { r.Group("/files/{name}/{version}/{user}/{channel}/{recipe_revision}", func() {
r.Group("/recipe/{filename}", func() { r.Group("/recipe/{filename}", func() {
r.Get("", conan.DownloadRecipeFile) r.Get("", conan.DownloadRecipeFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadRecipeFile) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.UploadRecipeFile)
}) })
r.Group("/package/{package_reference}/{package_revision}/{filename}", func() { r.Group("/package/{package_reference}/{package_revision}/{filename}", func() {
r.Get("", conan.DownloadPackageFile) r.Get("", conan.DownloadPackageFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadPackageFile) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.UploadPackageFile)
}) })
}, conan.ExtractPathParameters) }, conan.ExtractPathParameters)
}) })
@ -228,7 +244,7 @@ func CommonRoutes() *web.Route {
r.Get("", conan.ListRecipeRevisionFiles) r.Get("", conan.ListRecipeRevisionFiles)
r.Group("/{filename}", func() { r.Group("/{filename}", func() {
r.Get("", conan.DownloadRecipeFile) r.Get("", conan.DownloadRecipeFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadRecipeFile) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.UploadRecipeFile)
}) })
}) })
r.Group("/packages", func() { r.Group("/packages", func() {
@ -244,7 +260,7 @@ func CommonRoutes() *web.Route {
r.Get("", conan.ListPackageRevisionFiles) r.Get("", conan.ListPackageRevisionFiles)
r.Group("/{filename}", func() { r.Group("/{filename}", func() {
r.Get("", conan.DownloadPackageFile) r.Get("", conan.DownloadPackageFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadPackageFile) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.UploadPackageFile)
}) })
}) })
}) })
@ -281,7 +297,7 @@ func CommonRoutes() *web.Route {
conda.DownloadPackageFile(ctx) conda.DownloadPackageFile(ctx)
} }
}) })
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) { r.Put("/*", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), func(ctx *context.Context) {
m := uploadPattern.FindStringSubmatch(ctx.Params("*")) m := uploadPattern.FindStringSubmatch(ctx.Params("*"))
if len(m) == 0 { if len(m) == 0 {
ctx.Status(http.StatusNotFound) ctx.Status(http.StatusNotFound)
@ -301,7 +317,7 @@ func CommonRoutes() *web.Route {
r.Get("/PACKAGES{format}", cran.EnumerateSourcePackages) r.Get("/PACKAGES{format}", cran.EnumerateSourcePackages)
r.Get("/{filename}", cran.DownloadSourcePackageFile) r.Get("/{filename}", cran.DownloadSourcePackageFile)
}) })
r.Put("", reqPackageAccess(perm.AccessModeWrite), cran.UploadSourcePackageFile) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), cran.UploadSourcePackageFile)
}) })
r.Group("/bin", func() { r.Group("/bin", func() {
r.Group("/{platform}/contrib/{rversion}", func() { r.Group("/{platform}/contrib/{rversion}", func() {
@ -309,7 +325,7 @@ func CommonRoutes() *web.Route {
r.Get("/PACKAGES{format}", cran.EnumerateBinaryPackages) r.Get("/PACKAGES{format}", cran.EnumerateBinaryPackages)
r.Get("/{filename}", cran.DownloadBinaryPackageFile) r.Get("/{filename}", cran.DownloadBinaryPackageFile)
}) })
r.Put("", reqPackageAccess(perm.AccessModeWrite), cran.UploadBinaryPackageFile) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), cran.UploadBinaryPackageFile)
}) })
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/debian", func() { r.Group("/debian", func() {
@ -325,13 +341,13 @@ func CommonRoutes() *web.Route {
r.Group("/pool/{distribution}/{component}", func() { r.Group("/pool/{distribution}/{component}", func() {
r.Get("/{name}_{version}_{architecture}.deb", debian.DownloadPackageFile) r.Get("/{name}_{version}_{architecture}.deb", debian.DownloadPackageFile)
r.Group("", func() { r.Group("", func() {
r.Put("/upload", debian.UploadPackageFile) r.Put("/upload", enforcePackagesQuota(), debian.UploadPackageFile)
r.Delete("/{name}/{version}/{architecture}", debian.DeletePackageFile) r.Delete("/{name}/{version}/{architecture}", debian.DeletePackageFile)
}, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeWrite))
}) })
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/go", func() { r.Group("/go", func() {
r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage) r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), goproxy.UploadPackage)
r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) { r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) {
ctx.Status(http.StatusNotFound) ctx.Status(http.StatusNotFound)
}) })
@ -394,7 +410,7 @@ func CommonRoutes() *web.Route {
r.Group("/{filename}", func() { r.Group("/{filename}", func() {
r.Get("", generic.DownloadPackageFile) r.Get("", generic.DownloadPackageFile)
r.Group("", func() { r.Group("", func() {
r.Put("", generic.UploadPackage) r.Put("", enforcePackagesQuota(), generic.UploadPackage)
r.Delete("", generic.DeletePackageFile) r.Delete("", generic.DeletePackageFile)
}, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeWrite))
}) })
@ -403,10 +419,10 @@ func CommonRoutes() *web.Route {
r.Group("/helm", func() { r.Group("/helm", func() {
r.Get("/index.yaml", helm.Index) r.Get("/index.yaml", helm.Index)
r.Get("/{filename}", helm.DownloadPackageFile) r.Get("/{filename}", helm.DownloadPackageFile)
r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage) r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), helm.UploadPackage)
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/maven", func() { r.Group("/maven", func() {
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile) r.Put("/*", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), maven.UploadPackageFile)
r.Get("/*", maven.DownloadPackageFile) r.Get("/*", maven.DownloadPackageFile)
r.Head("/*", maven.ProvidePackageFileHeader) r.Head("/*", maven.ProvidePackageFileHeader)
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
@ -427,8 +443,8 @@ func CommonRoutes() *web.Route {
r.Get("/{version}/{filename}", nuget.DownloadPackageFile) r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
}) })
r.Group("", func() { r.Group("", func() {
r.Put("/", nuget.UploadPackage) r.Put("/", enforcePackagesQuota(), nuget.UploadPackage)
r.Put("/symbolpackage", nuget.UploadSymbolPackage) r.Put("/symbolpackage", enforcePackagesQuota(), nuget.UploadSymbolPackage)
r.Delete("/{id}/{version}", nuget.DeletePackage) r.Delete("/{id}/{version}", nuget.DeletePackage)
}, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeWrite))
r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile) r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile)
@ -450,7 +466,7 @@ func CommonRoutes() *web.Route {
r.Group("/npm", func() { r.Group("/npm", func() {
r.Group("/@{scope}/{id}", func() { r.Group("/@{scope}/{id}", func() {
r.Get("", npm.PackageMetadata) r.Get("", npm.PackageMetadata)
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), npm.UploadPackage)
r.Group("/-/{version}/{filename}", func() { r.Group("/-/{version}/{filename}", func() {
r.Get("", npm.DownloadPackageFile) r.Get("", npm.DownloadPackageFile)
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion) r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
@ -463,7 +479,7 @@ func CommonRoutes() *web.Route {
}) })
r.Group("/{id}", func() { r.Group("/{id}", func() {
r.Get("", npm.PackageMetadata) r.Get("", npm.PackageMetadata)
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), npm.UploadPackage)
r.Group("/-/{version}/{filename}", func() { r.Group("/-/{version}/{filename}", func() {
r.Get("", npm.DownloadPackageFile) r.Get("", npm.DownloadPackageFile)
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion) r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
@ -496,7 +512,7 @@ func CommonRoutes() *web.Route {
r.Group("/api/packages", func() { r.Group("/api/packages", func() {
r.Group("/versions/new", func() { r.Group("/versions/new", func() {
r.Get("", pub.RequestUpload) r.Get("", pub.RequestUpload)
r.Post("/upload", pub.UploadPackageFile) r.Post("/upload", enforcePackagesQuota(), pub.UploadPackageFile)
r.Get("/finalize/{id}/{version}", pub.FinalizePackage) r.Get("/finalize/{id}/{version}", pub.FinalizePackage)
}, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeWrite))
r.Group("/{id}", func() { r.Group("/{id}", func() {
@ -507,7 +523,7 @@ func CommonRoutes() *web.Route {
}) })
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/pypi", func() { r.Group("/pypi", func() {
r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile) r.Post("/", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), pypi.UploadPackageFile)
r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
r.Get("/simple/{id}", pypi.PackageMetadata) r.Get("/simple/{id}", pypi.PackageMetadata)
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
@ -556,6 +572,10 @@ func CommonRoutes() *web.Route {
if ctx.Written() { if ctx.Written() {
return return
} }
enforcePackagesQuota()(ctx)
if ctx.Written() {
return
}
ctx.SetParams("group", strings.Trim(m[1], "/")) ctx.SetParams("group", strings.Trim(m[1], "/"))
rpm.UploadPackageFile(ctx) rpm.UploadPackageFile(ctx)
return return
@ -591,7 +611,7 @@ func CommonRoutes() *web.Route {
r.Get("/quick/Marshal.4.8/{filename}", rubygems.ServePackageSpecification) r.Get("/quick/Marshal.4.8/{filename}", rubygems.ServePackageSpecification)
r.Get("/gems/{filename}", rubygems.DownloadPackageFile) r.Get("/gems/{filename}", rubygems.DownloadPackageFile)
r.Group("/api/v1/gems", func() { r.Group("/api/v1/gems", func() {
r.Post("/", rubygems.UploadPackageFile) r.Post("/", enforcePackagesQuota(), rubygems.UploadPackageFile)
r.Delete("/yank", rubygems.DeletePackage) r.Delete("/yank", rubygems.DeletePackage)
}, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeWrite))
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
@ -603,7 +623,7 @@ func CommonRoutes() *web.Route {
}, swift.CheckAcceptMediaType(swift.AcceptJSON)) }, swift.CheckAcceptMediaType(swift.AcceptJSON))
r.Group("/{version}", func() { r.Group("/{version}", func() {
r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest)
r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), enforcePackagesQuota(), swift.UploadPackageFile)
r.Get("", func(ctx *context.Context) { r.Get("", func(ctx *context.Context) {
// Can't use normal routes here: https://github.com/go-chi/chi/issues/781 // Can't use normal routes here: https://github.com/go-chi/chi/issues/781
@ -639,7 +659,7 @@ func CommonRoutes() *web.Route {
r.Get("", vagrant.EnumeratePackageVersions) r.Get("", vagrant.EnumeratePackageVersions)
r.Group("/{version}/{provider}", func() { r.Group("/{version}/{provider}", func() {
r.Get("", vagrant.DownloadPackageFile) r.Get("", vagrant.DownloadPackageFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), vagrant.UploadPackageFile) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), vagrant.UploadPackageFile)
}) })
}) })
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))

View file

@ -77,6 +77,7 @@ import (
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" 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" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -973,7 +974,7 @@ func Routes() *web.Route {
// (repo scope) // (repo scope)
m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos). m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos).
Post(bind(api.CreateRepoOption{}), repo.Create) Post(bind(api.CreateRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetUser), repo.Create)
// (repo scope) // (repo scope)
if !setting.Repository.DisableStars { if !setting.Repository.DisableStars {
@ -1104,7 +1105,7 @@ func Routes() *web.Route {
m.Get("", repo.ListBranches) m.Get("", repo.ListBranches)
m.Get("/*", repo.GetBranch) m.Get("/*", repo.GetBranch)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch) m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.CreateBranch)
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
m.Group("/branch_protections", func() { m.Group("/branch_protections", func() {
m.Get("", repo.ListBranchProtections) m.Get("", repo.ListBranchProtections)
@ -1118,7 +1119,7 @@ func Routes() *web.Route {
m.Group("/tags", func() { m.Group("/tags", func() {
m.Get("", repo.ListTags) m.Get("", repo.ListTags)
m.Get("/*", repo.GetTag) m.Get("/*", repo.GetTag)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag) 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) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag)
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true)) }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
m.Group("/tag_protections", func() { m.Group("/tag_protections", func() {
@ -1152,10 +1153,10 @@ func Routes() *web.Route {
m.Group("/wiki", func() { m.Group("/wiki", func() {
m.Combo("/page/{pageName}"). m.Combo("/page/{pageName}").
Get(repo.GetWikiPage). Get(repo.GetWikiPage).
Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage). 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) Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
m.Get("/revisions/{pageName}", repo.ListPageRevisions) m.Get("/revisions/{pageName}", repo.ListPageRevisions)
m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage) 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) m.Get("/pages", repo.ListWikiPages)
}, mustEnableWiki) }, mustEnableWiki)
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
@ -1172,15 +1173,15 @@ func Routes() *web.Route {
}, reqToken()) }, reqToken())
m.Group("/releases", func() { m.Group("/releases", func() {
m.Combo("").Get(repo.ListReleases). m.Combo("").Get(repo.ListReleases).
Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.CreateReleaseOption{}), repo.CreateRelease) 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.Combo("/latest").Get(repo.GetLatestRelease)
m.Group("/{id}", func() { m.Group("/{id}", func() {
m.Combo("").Get(repo.GetRelease). m.Combo("").Get(repo.GetRelease).
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.EditReleaseOption{}), repo.EditRelease). 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) Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease)
m.Group("/assets", func() { m.Group("/assets", func() {
m.Combo("").Get(repo.ListReleaseAttachments). m.Combo("").Get(repo.ListReleaseAttachments).
Post(reqToken(), reqRepoWriter(unit.TypeReleases), repo.CreateReleaseAttachment) Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsReleases, context.QuotaTargetRepo), repo.CreateReleaseAttachment)
m.Combo("/{attachment_id}").Get(repo.GetReleaseAttachment). m.Combo("/{attachment_id}").Get(repo.GetReleaseAttachment).
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), bind(api.EditAttachmentOptions{}), repo.EditReleaseAttachment). Patch(reqToken(), reqRepoWriter(unit.TypeReleases), bind(api.EditAttachmentOptions{}), repo.EditReleaseAttachment).
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseAttachment) Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseAttachment)
@ -1192,7 +1193,7 @@ func Routes() *web.Route {
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag) Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
}) })
}, reqRepoReader(unit.TypeReleases)) }, reqRepoReader(unit.TypeReleases))
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.MirrorSync) 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.Post("/push_mirrors-sync", reqAdmin(), reqToken(), mustNotBeArchived, repo.PushMirrorSync)
m.Group("/push_mirrors", func() { m.Group("/push_mirrors", func() {
m.Combo("").Get(repo.ListPushMirrors). m.Combo("").Get(repo.ListPushMirrors).
@ -1211,11 +1212,11 @@ func Routes() *web.Route {
m.Combo("").Get(repo.GetPullRequest). m.Combo("").Get(repo.GetPullRequest).
Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest) Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
m.Get(".{diffType:diff|patch}", repo.DownloadPullDiffOrPatch) m.Get(".{diffType:diff|patch}", repo.DownloadPullDiffOrPatch)
m.Post("/update", reqToken(), repo.UpdatePullRequest) m.Post("/update", reqToken(), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.UpdatePullRequest)
m.Get("/commits", repo.GetPullRequestCommits) m.Get("/commits", repo.GetPullRequestCommits)
m.Get("/files", repo.GetPullRequestFiles) m.Get("/files", repo.GetPullRequestFiles)
m.Combo("/merge").Get(repo.IsPullRequestMerged). m.Combo("/merge").Get(repo.IsPullRequestMerged).
Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest). Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.MergePullRequest).
Delete(reqToken(), mustNotBeArchived, repo.CancelScheduledAutoMerge) Delete(reqToken(), mustNotBeArchived, repo.CancelScheduledAutoMerge)
m.Group("/reviews", func() { m.Group("/reviews", func() {
m.Combo(""). m.Combo("").
@ -1270,15 +1271,15 @@ func Routes() *web.Route {
m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Get("/tags/{sha}", repo.GetAnnotatedTag)
m.Get("/notes/{sha}", repo.GetNote) m.Get("/notes/{sha}", repo.GetNote)
}, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode))
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, repo.ApplyDiffPatch) 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.Group("/contents", func() {
m.Get("", repo.GetContentsList) m.Get("", repo.GetContentsList)
m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.ChangeFiles) m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.ChangeFiles)
m.Get("/*", repo.GetContents) m.Get("/*", repo.GetContents)
m.Group("/*", func() { m.Group("/*", func() {
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.CreateFile) m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.CreateFile)
m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.UpdateFile) m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.UpdateFile)
m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile) m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.DeleteFile)
}, reqToken()) }, reqToken())
}, reqRepoReader(unit.TypeCode)) }, reqRepoReader(unit.TypeCode))
m.Get("/signing-key.gpg", misc.SigningKey) m.Get("/signing-key.gpg", misc.SigningKey)
@ -1335,7 +1336,7 @@ func Routes() *web.Route {
m.Group("/assets", func() { m.Group("/assets", func() {
m.Combo(""). m.Combo("").
Get(repo.ListIssueCommentAttachments). Get(repo.ListIssueCommentAttachments).
Post(reqToken(), mustNotBeArchived, repo.CreateIssueCommentAttachment) Post(reqToken(), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsIssues, context.QuotaTargetRepo), repo.CreateIssueCommentAttachment)
m.Combo("/{attachment_id}"). m.Combo("/{attachment_id}").
Get(repo.GetIssueCommentAttachment). Get(repo.GetIssueCommentAttachment).
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment). Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
@ -1387,7 +1388,7 @@ func Routes() *web.Route {
m.Group("/assets", func() { m.Group("/assets", func() {
m.Combo(""). m.Combo("").
Get(repo.ListIssueAttachments). Get(repo.ListIssueAttachments).
Post(reqToken(), mustNotBeArchived, repo.CreateIssueAttachment) Post(reqToken(), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsIssues, context.QuotaTargetRepo), repo.CreateIssueAttachment)
m.Combo("/{attachment_id}"). m.Combo("/{attachment_id}").
Get(repo.GetIssueAttachment). Get(repo.GetIssueAttachment).
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment). Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
@ -1449,7 +1450,7 @@ func Routes() *web.Route {
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
Delete(reqToken(), reqOrgOwnership(), org.Delete) Delete(reqToken(), reqOrgOwnership(), org.Delete)
m.Combo("/repos").Get(user.ListOrgRepos). m.Combo("/repos").Get(user.ListOrgRepos).
Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo) Post(reqToken(), bind(api.CreateRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetOrg), repo.CreateOrgRepo)
m.Group("/members", func() { m.Group("/members", func() {
m.Get("", reqToken(), org.ListMembers) m.Get("", reqToken(), org.ListMembers)
m.Combo("/{username}").Get(reqToken(), org.IsMember). m.Combo("/{username}").Get(reqToken(), org.IsMember).

View file

@ -210,6 +210,8 @@ func CreateBranch(ctx *context.APIContext) {
// description: The old branch does not exist. // description: The old branch does not exist.
// "409": // "409":
// description: The branch with the same name already exists. // description: The branch with the same name already exists.
// "413":
// "$ref": "#/responses/quotaExceeded"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"

View file

@ -477,6 +477,8 @@ func ChangeFiles(ctx *context.APIContext) {
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "423": // "423":
@ -579,6 +581,8 @@ func CreateFile(ctx *context.APIContext) {
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "423": // "423":
@ -677,6 +681,8 @@ func UpdateFile(ctx *context.APIContext) {
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "423": // "423":
@ -842,6 +848,8 @@ func DeleteFile(ctx *context.APIContext) {
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "404": // "404":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"

View file

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" 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" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
@ -105,6 +106,8 @@ func CreateFork(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "409": // "409":
// description: The repository with the same name already exists. // description: The repository with the same name already exists.
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
@ -134,6 +137,10 @@ func CreateFork(ctx *context.APIContext) {
forker = org.AsUser() forker = org.AsUser()
} }
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, forker.ID, forker.Name) {
return
}
var name string var name string
if form.Name == nil { if form.Name == nil {
name = repo.Name name = repo.Name

View file

@ -160,6 +160,8 @@ func CreateIssueAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "404": // "404":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
// "423": // "423":
@ -269,6 +271,8 @@ func EditIssueAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/Attachment" // "$ref": "#/responses/Attachment"
// "404": // "404":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"

View file

@ -157,6 +157,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "404": // "404":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
// "423": // "423":
@ -274,6 +276,8 @@ func EditIssueCommentAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/Attachment" // "$ref": "#/responses/Attachment"
// "404": // "404":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"
attach := getIssueCommentAttachmentSafeWrite(ctx) attach := getIssueCommentAttachmentSafeWrite(ctx)

View file

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" 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" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
@ -54,6 +55,8 @@ func Migrate(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
// "409": // "409":
// description: The repository with the same name already exists. // description: The repository with the same name already exists.
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
@ -85,6 +88,10 @@ func Migrate(ctx *context.APIContext) {
return return
} }
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, repoOwner.ID, repoOwner.Name) {
return
}
if !ctx.Doer.IsAdmin { if !ctx.Doer.IsAdmin {
if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID { if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID {
ctx.Error(http.StatusForbidden, "", "Given user is not an organization.") ctx.Error(http.StatusForbidden, "", "Given user is not an organization.")

View file

@ -50,6 +50,8 @@ func MirrorSync(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
@ -103,6 +105,8 @@ func PushMirrorSync(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
if !setting.Mirror.Enabled { if !setting.Mirror.Enabled {
ctx.Error(http.StatusBadRequest, "PushMirrorSync", "Mirror feature is disabled") ctx.Error(http.StatusBadRequest, "PushMirrorSync", "Mirror feature is disabled")
@ -279,6 +283,8 @@ func AddPushMirror(ctx *context.APIContext) {
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
if !setting.Mirror.Enabled { if !setting.Mirror.Enabled {
ctx.Error(http.StatusBadRequest, "AddPushMirror", "Mirror feature is disabled") ctx.Error(http.StatusBadRequest, "AddPushMirror", "Mirror feature is disabled")

View file

@ -47,6 +47,8 @@ func ApplyDiffPatch(ctx *context.APIContext) {
// "$ref": "#/responses/FileResponse" // "$ref": "#/responses/FileResponse"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"
apiOpts := web.GetForm(ctx).(*api.ApplyDiffPatchFileOptions) apiOpts := web.GetForm(ctx).(*api.ApplyDiffPatchFileOptions)

View file

@ -387,6 +387,8 @@ func CreatePullRequest(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "409": // "409":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
// "423": // "423":
@ -857,6 +859,8 @@ func MergePullRequest(ctx *context.APIContext) {
// "$ref": "#/responses/empty" // "$ref": "#/responses/empty"
// "409": // "409":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"
@ -1218,6 +1222,8 @@ func UpdatePullRequest(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "409": // "409":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"

View file

@ -201,6 +201,8 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
// Check if attachments are enabled // Check if attachments are enabled
if !setting.Attachment.Enabled { if !setting.Attachment.Enabled {
@ -348,6 +350,8 @@ func EditReleaseAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/Attachment" // "$ref": "#/responses/Attachment"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
form := web.GetForm(ctx).(*api.EditAttachmentOptions) form := web.GetForm(ctx).(*api.EditAttachmentOptions)

View file

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" 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" repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit" unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -302,6 +303,8 @@ func Create(ctx *context.APIContext) {
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "409": // "409":
// description: The repository with the same name already exists. // description: The repository with the same name already exists.
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
opt := web.GetForm(ctx).(*api.CreateRepoOption) opt := web.GetForm(ctx).(*api.CreateRepoOption)
@ -346,6 +349,8 @@ func Generate(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "409": // "409":
// description: The repository with the same name already exists. // description: The repository with the same name already exists.
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.GenerateRepoOption) form := web.GetForm(ctx).(*api.GenerateRepoOption)
@ -412,6 +417,10 @@ func Generate(ctx *context.APIContext) {
} }
} }
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) {
return
}
repo, err := repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, ctx.Repo.Repository, opts) repo, err := repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, ctx.Repo.Repository, opts)
if err != nil { if err != nil {
if repo_model.IsErrRepoAlreadyExist(err) { if repo_model.IsErrRepoAlreadyExist(err) {

View file

@ -208,6 +208,8 @@ func CreateTag(ctx *context.APIContext) {
// "$ref": "#/responses/empty" // "$ref": "#/responses/empty"
// "409": // "409":
// "$ref": "#/responses/conflict" // "$ref": "#/responses/conflict"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
// "423": // "423":

View file

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" 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" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -53,6 +54,8 @@ func Transfer(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422": // "422":
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
@ -76,6 +79,10 @@ func Transfer(ctx *context.APIContext) {
} }
} }
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, newOwner.ID, newOwner.Name) {
return
}
var teams []*organization.Team var teams []*organization.Team
if opts.TeamIDs != nil { if opts.TeamIDs != nil {
if !newOwner.IsOrganization() { if !newOwner.IsOrganization() {
@ -162,6 +169,8 @@ func AcceptTransfer(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
err := acceptOrRejectRepoTransfer(ctx, true) err := acceptOrRejectRepoTransfer(ctx, true)
if ctx.Written() { if ctx.Written() {
@ -233,6 +242,11 @@ func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error {
} }
if accept { if accept {
recipient := repoTransfer.Recipient
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, recipient.ID, recipient.Name) {
return nil
}
return repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams) return repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams)
} }

View file

@ -53,6 +53,8 @@ func NewWikiPage(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"
@ -131,6 +133,8 @@ func EditWikiPage(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"

View file

@ -15,11 +15,13 @@ import (
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
perm_model "code.gitea.io/gitea/models/perm" perm_model "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
quota_model "code.gitea.io/gitea/models/quota"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
gitea_context "code.gitea.io/gitea/services/context" gitea_context "code.gitea.io/gitea/services/context"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
@ -47,6 +49,8 @@ type preReceiveContext struct {
opts *private.HookOptions opts *private.HookOptions
isOverQuota bool
branchName string branchName string
} }
@ -140,6 +144,36 @@ func (ctx *preReceiveContext) assertPushOptions() bool {
return true return true
} }
func (ctx *preReceiveContext) checkQuota() error {
if !setting.Quota.Enabled {
ctx.isOverQuota = false
return nil
}
if !ctx.loadPusherAndPermission() {
ctx.isOverQuota = true
return nil
}
ok, err := quota_model.EvaluateForUser(ctx, ctx.PrivateContext.Repo.Repository.OwnerID, quota_model.LimitSubjectSizeReposAll)
if err != nil {
log.Error("quota_model.EvaluateForUser: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
UserMsg: "Error checking user quota",
})
return err
}
ctx.isOverQuota = !ok
return nil
}
func (ctx *preReceiveContext) quotaExceeded() {
ctx.JSON(http.StatusRequestEntityTooLarge, private.Response{
UserMsg: "Quota exceeded",
})
}
// HookPreReceive checks whether a individual commit is acceptable // HookPreReceive checks whether a individual commit is acceptable
func HookPreReceive(ctx *gitea_context.PrivateContext) { func HookPreReceive(ctx *gitea_context.PrivateContext) {
opts := web.GetForm(ctx).(*private.HookOptions) opts := web.GetForm(ctx).(*private.HookOptions)
@ -156,6 +190,10 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
} }
log.Trace("Git push options validation succeeded") log.Trace("Git push options validation succeeded")
if err := ourCtx.checkQuota(); err != nil {
return
}
// Iterate across the provided old commit IDs // Iterate across the provided old commit IDs
for i := range opts.OldCommitIDs { for i := range opts.OldCommitIDs {
oldCommitID := opts.OldCommitIDs[i] oldCommitID := opts.OldCommitIDs[i]
@ -170,6 +208,10 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
case git.SupportProcReceive && refFullName.IsFor(): case git.SupportProcReceive && refFullName.IsFor():
preReceiveFor(ourCtx, oldCommitID, newCommitID, refFullName) preReceiveFor(ourCtx, oldCommitID, newCommitID, refFullName)
default: default:
if ourCtx.isOverQuota {
ourCtx.quotaExceeded()
return
}
ourCtx.AssertCanWriteCode() ourCtx.AssertCanWriteCode()
} }
if ctx.Written() { if ctx.Written() {
@ -211,6 +253,11 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
// Allow pushes to non-protected branches // Allow pushes to non-protected branches
if protectBranch == nil { if protectBranch == nil {
// ...unless the user is over quota, and the operation is not a delete
if newCommitID != objectFormat.EmptyObjectID().String() && ctx.isOverQuota {
ctx.quotaExceeded()
}
return return
} }
protectBranch.Repo = repo protectBranch.Repo = repo
@ -452,6 +499,15 @@ func preReceiveTag(ctx *preReceiveContext, oldCommitID, newCommitID string, refF
}) })
return return
} }
// If the user is over quota, and the push isn't a tag deletion, deny it
if ctx.isOverQuota {
objectFormat := ctx.Repo.GetObjectFormat()
if newCommitID != objectFormat.EmptyObjectID().String() {
ctx.quotaExceeded()
return
}
}
} }
func preReceiveFor(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) { //nolint:unparam func preReceiveFor(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) { //nolint:unparam

View file

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
admin_model "code.gitea.io/gitea/models/admin" admin_model "code.gitea.io/gitea/models/admin"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
quota_model "code.gitea.io/gitea/models/quota"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
@ -170,6 +171,10 @@ func MigratePost(ctx *context.Context) {
tpl := base.TplName("repo/migrate/" + form.Service.Name()) tpl := base.TplName("repo/migrate/" + form.Service.Name())
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) {
return
}
if ctx.HasError() { if ctx.HasError() {
ctx.HTML(http.StatusOK, tpl) ctx.HTML(http.StatusOK, tpl)
return return
@ -260,6 +265,25 @@ func setMigrationContextData(ctx *context.Context, serviceType structs.GitServic
} }
func MigrateRetryPost(ctx *context.Context) { func MigrateRetryPost(ctx *context.Context) {
ok, err := quota_model.EvaluateForUser(ctx, ctx.Repo.Repository.OwnerID, quota_model.LimitSubjectSizeReposAll)
if err != nil {
log.Error("quota_model.EvaluateForUser: %v", err)
ctx.ServerError("quota_model.EvaluateForUser", err)
return
}
if !ok {
if err := task.SetMigrateTaskMessage(ctx, ctx.Repo.Repository.ID, ctx.Locale.TrString("repo.settings.pull_mirror_sync_quota_exceeded")); err != nil {
log.Error("SetMigrateTaskMessage failed: %v", err)
ctx.ServerError("task.SetMigrateTaskMessage", err)
return
}
ctx.JSON(http.StatusRequestEntityTooLarge, map[string]any{
"ok": false,
"error": ctx.Tr("repo.settings.pull_mirror_sync_quota_exceeded"),
})
return
}
if err := task.RetryMigrateTask(ctx, ctx.Repo.Repository.ID); err != nil { if err := task.RetryMigrateTask(ctx, ctx.Repo.Repository.ID); err != nil {
log.Error("Retry task failed: %v", err) log.Error("Retry task failed: %v", err)
ctx.ServerError("task.RetryMigrateTask", err) ctx.ServerError("task.RetryMigrateTask", err)

View file

@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
pull_model "code.gitea.io/gitea/models/pull" pull_model "code.gitea.io/gitea/models/pull"
quota_model "code.gitea.io/gitea/models/quota"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -250,6 +251,10 @@ func ForkPost(ctx *context.Context) {
ctx.Data["ContextUser"] = ctxUser ctx.Data["ContextUser"] = ctxUser
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) {
return
}
if ctx.HasError() { if ctx.HasError() {
ctx.HTML(http.StatusOK, tplFork) ctx.HTML(http.StatusOK, tplFork)
return return

View file

@ -17,6 +17,7 @@ import (
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access" 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" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -240,6 +241,10 @@ func CreatePost(ctx *context.Context) {
} }
ctx.Data["ContextUser"] = ctxUser ctx.Data["ContextUser"] = ctxUser
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) {
return
}
if ctx.HasError() { if ctx.HasError() {
ctx.HTML(http.StatusOK, tplCreate) ctx.HTML(http.StatusOK, tplCreate)
return return
@ -363,49 +368,56 @@ func ActionTransfer(accept bool) func(ctx *context.Context) {
action = "reject_transfer" action = "reject_transfer"
} }
err := acceptOrRejectRepoTransfer(ctx, accept) ok, err := acceptOrRejectRepoTransfer(ctx, accept)
if err != nil { if err != nil {
ctx.ServerError(fmt.Sprintf("Action (%s)", action), err) ctx.ServerError(fmt.Sprintf("Action (%s)", action), err)
return return
} }
if !ok {
return
}
ctx.RedirectToFirst(ctx.FormString("redirect_to"), ctx.Repo.RepoLink) ctx.RedirectToFirst(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
} }
} }
func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error { func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) (bool, error) {
repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
if err != nil { if err != nil {
return err return false, err
} }
if err := repoTransfer.LoadAttributes(ctx); err != nil { if err := repoTransfer.LoadAttributes(ctx); err != nil {
return err return false, err
} }
if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) { if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) {
return errors.New("user does not have enough permissions") return false, errors.New("user does not have enough permissions")
} }
if accept { if accept {
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctx.Doer.ID, ctx.Doer.Name) {
return false, nil
}
if ctx.Repo.GitRepo != nil { if ctx.Repo.GitRepo != nil {
ctx.Repo.GitRepo.Close() ctx.Repo.GitRepo.Close()
ctx.Repo.GitRepo = nil ctx.Repo.GitRepo = nil
} }
if err := repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil { if err := repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
return err return false, err
} }
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success")) ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
} else { } else {
if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil { if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil {
return err return false, err
} }
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected")) ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
} }
ctx.Redirect(ctx.Repo.Repository.Link()) ctx.Redirect(ctx.Repo.Repository.Link())
return nil return true, nil
} }
// RedirectDownload return a file based on the following infos: // RedirectDownload return a file based on the following infos:

View file

@ -17,6 +17,7 @@ import (
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
quota_model "code.gitea.io/gitea/models/quota"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit" unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -518,6 +519,20 @@ func SettingsPost(ctx *context.Context) {
return return
} }
ok, err := quota_model.EvaluateForUser(ctx, repo.OwnerID, quota_model.LimitSubjectSizeReposAll)
if err != nil {
ctx.ServerError("quota_model.EvaluateForUser", err)
return
}
if !ok {
// This section doesn't require repo_name/RepoName to be set in the form, don't show it
// as an error on the UI for this action
ctx.Data["Err_RepoName"] = nil
ctx.RenderWithErr(ctx.Tr("repo.settings.pull_mirror_sync_quota_exceeded"), tplSettingsOptions, &form)
return
}
mirror_service.AddPullMirrorToQueue(repo.ID) mirror_service.AddPullMirrorToQueue(repo.ID)
ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL)) ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
@ -828,6 +843,17 @@ func SettingsPost(ctx *context.Context) {
} }
} }
// Check the quota of the new owner
ok, err := quota_model.EvaluateForUser(ctx, newOwner.ID, quota_model.LimitSubjectSizeReposAll)
if err != nil {
ctx.ServerError("quota_model.EvaluateForUser", err)
return
}
if !ok {
ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_quota_exceeded", newOwner.Name), tplSettingsOptions, &form)
return
}
// Close the GitRepo if open // Close the GitRepo if open
if ctx.Repo.GitRepo != nil { if ctx.Repo.GitRepo != nil {
ctx.Repo.GitRepo.Close() ctx.Repo.GitRepo.Close()

View file

@ -11,6 +11,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
quota_model "code.gitea.io/gitea/models/quota"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/metrics" "code.gitea.io/gitea/modules/metrics"
@ -1196,7 +1197,7 @@ func registerRoutes(m *web.Route) {
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus) m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues) m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.SetShowOutdatedComments, repo.UpdateResolveConversation) m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.SetShowOutdatedComments, repo.UpdateResolveConversation)
m.Post("/attachments", repo.UploadIssueAttachment) m.Post("/attachments", context.EnforceQuotaWeb(quota_model.LimitSubjectSizeAssetsAttachmentsIssues, context.QuotaTargetRepo), repo.UploadIssueAttachment)
m.Post("/attachments/remove", repo.DeleteAttachment) m.Post("/attachments/remove", repo.DeleteAttachment)
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin) m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove) m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
@ -1244,9 +1245,9 @@ func registerRoutes(m *web.Route) {
Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
m.Combo("/_cherrypick/{sha:([a-f0-9]{4,64})}/*").Get(repo.CherryPick). m.Combo("/_cherrypick/{sha:([a-f0-9]{4,64})}/*").Get(repo.CherryPick).
Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost) Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost)
}, repo.MustBeEditable, repo.CommonEditorData) }, repo.MustBeEditable, repo.CommonEditorData, context.EnforceQuotaWeb(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo))
m.Group("", func() { m.Group("", func() {
m.Post("/upload-file", repo.UploadFileToServer) m.Post("/upload-file", context.EnforceQuotaWeb(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.UploadFileToServer)
m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer) m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
}, repo.MustBeEditable, repo.MustBeAbleToUpload) }, repo.MustBeEditable, repo.MustBeAbleToUpload)
}, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived()) }, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived())
@ -1256,7 +1257,7 @@ func registerRoutes(m *web.Route) {
m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch) m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch)
m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch) m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch)
m.Post("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.CreateBranch) m.Post("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.CreateBranch)
}, web.Bind(forms.NewBranchForm{})) }, web.Bind(forms.NewBranchForm{}), context.EnforceQuotaWeb(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo))
m.Post("/delete", repo.DeleteBranchPost) m.Post("/delete", repo.DeleteBranchPost)
m.Post("/restore", repo.RestoreBranchPost) m.Post("/restore", repo.RestoreBranchPost)
}, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty) }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
@ -1288,16 +1289,17 @@ func registerRoutes(m *web.Route) {
m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment) m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment)
m.Get("/releases/download/{vTag}/{fileName}", repo.MustBeNotEmpty, repo.RedirectDownload) m.Get("/releases/download/{vTag}/{fileName}", repo.MustBeNotEmpty, repo.RedirectDownload)
m.Group("/releases", func() { m.Group("/releases", func() {
m.Get("/new", repo.NewRelease) m.Combo("/new", context.EnforceQuotaWeb(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo)).
m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost) Get(repo.NewRelease).
Post(web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)
m.Post("/delete", repo.DeleteRelease) m.Post("/delete", repo.DeleteRelease)
m.Post("/attachments", repo.UploadReleaseAttachment) m.Post("/attachments", context.EnforceQuotaWeb(quota_model.LimitSubjectSizeAssetsAttachmentsReleases, context.QuotaTargetRepo), repo.UploadReleaseAttachment)
m.Post("/attachments/remove", repo.DeleteAttachment) m.Post("/attachments/remove", repo.DeleteAttachment)
}, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef()) }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef())
m.Group("/releases", func() { m.Group("/releases", func() {
m.Get("/edit/*", repo.EditRelease) m.Get("/edit/*", repo.EditRelease)
m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost) m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost)
}, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache) }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache, context.EnforceQuotaWeb(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo))
}, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader) }, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader)
// to maintain compatibility with old attachments // to maintain compatibility with old attachments
@ -1410,10 +1412,10 @@ func registerRoutes(m *web.Route) {
m.Group("/wiki", func() { m.Group("/wiki", func() {
m.Combo("/"). m.Combo("/").
Get(repo.Wiki). Get(repo.Wiki).
Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), context.EnforceQuotaWeb(quota_model.LimitSubjectSizeWiki, context.QuotaTargetRepo), repo.WikiPost)
m.Combo("/*"). m.Combo("/*").
Get(repo.Wiki). Get(repo.Wiki).
Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), context.EnforceQuotaWeb(quota_model.LimitSubjectSizeWiki, context.QuotaTargetRepo), repo.WikiPost)
m.Get("/commit/{sha:[a-f0-9]{4,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) m.Get("/commit/{sha:[a-f0-9]{4,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
m.Get("/commit/{sha:[a-f0-9]{4,64}}.{ext:patch|diff}", repo.RawDiff) m.Get("/commit/{sha:[a-f0-9]{4,64}}.{ext:patch|diff}", repo.RawDiff)
}, repo.MustEnableWiki, func(ctx *context.Context) { }, repo.MustEnableWiki, func(ctx *context.Context) {
@ -1490,7 +1492,7 @@ func registerRoutes(m *web.Route) {
m.Get("/list", context.RepoRef(), repo.GetPullCommits) m.Get("/list", context.RepoRef(), repo.GetPullCommits)
m.Get("/{sha:[a-f0-9]{4,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit) m.Get("/{sha:[a-f0-9]{4,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
}) })
m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest) m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), context.EnforceQuotaWeb(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.MergePullRequest)
m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest) m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
m.Post("/update", repo.UpdatePullRequest) m.Post("/update", repo.UpdatePullRequest)
m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits) m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits)

View file

@ -4,11 +4,30 @@
package context package context
import ( import (
"context"
"net/http" "net/http"
"strings"
quota_model "code.gitea.io/gitea/models/quota" quota_model "code.gitea.io/gitea/models/quota"
"code.gitea.io/gitea/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 // QuotaGroupAssignmentAPI returns a middleware to handle context-quota-group assignment for api routes
func QuotaGroupAssignmentAPI() func(ctx *APIContext) { func QuotaGroupAssignmentAPI() func(ctx *APIContext) {
return func(ctx *APIContext) { return func(ctx *APIContext) {
@ -42,3 +61,140 @@ func QuotaRuleAssignmentAPI() func(ctx *APIContext) {
ctx.QuotaRule = rule 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.Base.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.Base.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)
}

View file

@ -23,6 +23,7 @@ import (
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" 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" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -179,6 +180,18 @@ func BatchHandler(ctx *context.Context) {
return return
} }
if isUpload {
ok, err := quota_model.EvaluateForUser(ctx, ctx.Doer.ID, quota_model.LimitSubjectSizeGitLFS)
if err != nil {
log.Error("quota_model.EvaluateForUser: %v", err)
writeStatus(ctx, http.StatusInternalServerError)
return
}
if !ok {
writeStatusMessage(ctx, http.StatusRequestEntityTooLarge, "quota exceeded")
}
}
contentStore := lfs_module.NewContentStore() contentStore := lfs_module.NewContentStore()
var responseObjects []*lfs_module.ObjectResponse var responseObjects []*lfs_module.ObjectResponse
@ -297,6 +310,18 @@ func UploadHandler(ctx *context.Context) {
return return
} }
if exists {
ok, err := quota_model.EvaluateForUser(ctx, ctx.Doer.ID, quota_model.LimitSubjectSizeGitLFS)
if err != nil {
log.Error("quota_model.EvaluateForUser: %v", err)
writeStatus(ctx, http.StatusInternalServerError)
return
}
if !ok {
writeStatusMessage(ctx, http.StatusRequestEntityTooLarge, "quota exceeded")
}
}
uploadOrVerify := func() error { uploadOrVerify := func() error {
if exists { if exists {
accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid) accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)

View file

@ -7,6 +7,7 @@ import (
"context" "context"
"fmt" "fmt"
quota_model "code.gitea.io/gitea/models/quota"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -73,6 +74,19 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
default: default:
} }
// Check if the repo's owner is over quota, for pull mirrors
if mirrorType == PullMirrorType {
ok, err := quota_model.EvaluateForUser(ctx, repo.OwnerID, quota_model.LimitSubjectSizeReposAll)
if err != nil {
log.Error("quota_model.EvaluateForUser: %v", err)
return err
}
if !ok {
log.Trace("Owner quota exceeded for %-v, not syncing", repo)
return nil
}
}
// Push to the Queue // Push to the Queue
if err := PushToQueue(mirrorType, referenceID); err != nil { if err := PushToQueue(mirrorType, referenceID); err != nil {
if err == queue.ErrAlreadyInQueue { if err == queue.ErrAlreadyInQueue {

View file

@ -152,3 +152,18 @@ func RetryMigrateTask(ctx context.Context, repoID int64) error {
return taskQueue.Push(migratingTask) return taskQueue.Push(migratingTask)
} }
func SetMigrateTaskMessage(ctx context.Context, repoID int64, message string) error {
migratingTask, err := admin_model.GetMigratingTask(ctx, repoID)
if err != nil {
log.Error("GetMigratingTask: %v", err)
return err
}
migratingTask.Message = message
if err = migratingTask.UpdateCols(ctx, "message"); err != nil {
log.Error("task.UpdateCols failed: %v", err)
return err
}
return nil
}

11
templates/status/413.tmpl Normal file
View file

@ -0,0 +1,11 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content ui center tw-w-screen {{if .IsRepo}}repository{{end}}">
{{if .IsRepo}}{{template "repo/header" .}}{{end}}
<div class="ui container center">
<h1 style="margin-top: 100px" class="error-code">413</h1>
<p>{{ctx.Locale.Tr "error413"}}</p>
<div class="divider"></div>
<br>
</div>
</div>
{{template "base/footer" .}}

View file

@ -4306,6 +4306,9 @@
"409": { "409": {
"description": "The repository with the same name already exists." "description": "The repository with the same name already exists."
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/validationError" "$ref": "#/responses/validationError"
} }
@ -5612,6 +5615,9 @@
"409": { "409": {
"description": "The branch with the same name already exists." "description": "The branch with the same name already exists."
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"423": { "423": {
"$ref": "#/responses/repoArchivedError" "$ref": "#/responses/repoArchivedError"
} }
@ -6348,6 +6354,9 @@
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
@ -6458,6 +6467,9 @@
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
@ -6519,6 +6531,9 @@
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
@ -6583,6 +6598,9 @@
"404": { "404": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"423": { "423": {
"$ref": "#/responses/repoArchivedError" "$ref": "#/responses/repoArchivedError"
} }
@ -6633,6 +6651,9 @@
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"423": { "423": {
"$ref": "#/responses/repoArchivedError" "$ref": "#/responses/repoArchivedError"
} }
@ -7034,6 +7055,9 @@
"409": { "409": {
"description": "The repository with the same name already exists." "description": "The repository with the same name already exists."
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/validationError" "$ref": "#/responses/validationError"
} }
@ -8506,6 +8530,9 @@
"404": { "404": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/validationError" "$ref": "#/responses/validationError"
}, },
@ -8677,6 +8704,9 @@
"404": { "404": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"423": { "423": {
"$ref": "#/responses/repoArchivedError" "$ref": "#/responses/repoArchivedError"
} }
@ -9135,6 +9165,9 @@
"404": { "404": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/validationError" "$ref": "#/responses/validationError"
}, },
@ -9306,6 +9339,9 @@
"404": { "404": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"423": { "423": {
"$ref": "#/responses/repoArchivedError" "$ref": "#/responses/repoArchivedError"
} }
@ -11979,6 +12015,9 @@
}, },
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
},
"413": {
"$ref": "#/responses/quotaExceeded"
} }
} }
} }
@ -12311,6 +12350,9 @@
"409": { "409": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/validationError" "$ref": "#/responses/validationError"
}, },
@ -12813,6 +12855,9 @@
"409": { "409": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"423": { "423": {
"$ref": "#/responses/repoArchivedError" "$ref": "#/responses/repoArchivedError"
} }
@ -13671,6 +13716,9 @@
"409": { "409": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/validationError" "$ref": "#/responses/validationError"
} }
@ -13777,6 +13825,9 @@
}, },
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
},
"413": {
"$ref": "#/responses/quotaExceeded"
} }
} }
} }
@ -13819,6 +13870,9 @@
}, },
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
},
"413": {
"$ref": "#/responses/quotaExceeded"
} }
} }
} }
@ -14443,6 +14497,9 @@
}, },
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
},
"413": {
"$ref": "#/responses/quotaExceeded"
} }
} }
} }
@ -14605,6 +14662,9 @@
}, },
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
},
"413": {
"$ref": "#/responses/quotaExceeded"
} }
} }
} }
@ -15359,6 +15419,9 @@
"409": { "409": {
"$ref": "#/responses/conflict" "$ref": "#/responses/conflict"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/validationError" "$ref": "#/responses/validationError"
}, },
@ -15991,6 +16054,9 @@
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/validationError" "$ref": "#/responses/validationError"
} }
@ -16032,6 +16098,9 @@
}, },
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
},
"413": {
"$ref": "#/responses/quotaExceeded"
} }
} }
} }
@ -16121,6 +16190,9 @@
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"423": { "423": {
"$ref": "#/responses/repoArchivedError" "$ref": "#/responses/repoArchivedError"
} }
@ -16265,6 +16337,9 @@
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"423": { "423": {
"$ref": "#/responses/repoArchivedError" "$ref": "#/responses/repoArchivedError"
} }
@ -16417,6 +16492,9 @@
"409": { "409": {
"description": "The repository with the same name already exists." "description": "The repository with the same name already exists."
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/validationError" "$ref": "#/responses/validationError"
} }
@ -18514,6 +18592,9 @@
"409": { "409": {
"description": "The repository with the same name already exists." "description": "The repository with the same name already exists."
}, },
"413": {
"$ref": "#/responses/quotaExceeded"
},
"422": { "422": {
"$ref": "#/responses/validationError" "$ref": "#/responses/validationError"
} }
@ -28110,6 +28191,21 @@
"$ref": "#/definitions/SetUserQuotaGroupsOptions" "$ref": "#/definitions/SetUserQuotaGroupsOptions"
} }
}, },
"quotaExceeded": {
"description": "QuotaExceeded",
"headers": {
"message": {
"type": "string"
},
"user_id": {
"type": "integer",
"format": "int64"
},
"username": {
"type": "string"
}
}
},
"redirect": { "redirect": {
"description": "APIRedirect is a redirect response" "description": "APIRedirect is a redirect response"
}, },

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff