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

@ -71,6 +71,7 @@ import (
"code.gitea.io/gitea/models/actions"
"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/log"
"code.gitea.io/gitea/modules/setting"
@ -240,6 +241,18 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
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
fileRealTotalSize, contentLength := getUploadFileSize(ctx)

View file

@ -92,6 +92,7 @@ import (
"code.gitea.io/gitea/models/actions"
"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/setting"
"code.gitea.io/gitea/modules/storage"
@ -290,6 +291,18 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
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")
switch comp {
case "block", "appendBlock":

View file

@ -10,6 +10,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"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/setting"
"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) {
if setting.Service.EnableReverseProxyAuth {
authMethods = append(authMethods, &auth.ReverseProxy{})
@ -111,7 +127,7 @@ func CommonRoutes() *web.Route {
r.Group("/alpine", func() {
r.Get("/key", alpine.GetRepositoryKey)
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.Get("/APKINDEX.tar.gz", alpine.GetRepositoryFile)
r.Group("/{filename}", func() {
@ -124,12 +140,12 @@ func CommonRoutes() *web.Route {
r.Group("/cargo", func() {
r.Group("/api/v1/crates", func() {
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("/{version}", func() {
r.Get("/download", cargo.DownloadPackageFile)
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)
})
@ -147,7 +163,7 @@ func CommonRoutes() *web.Route {
r.Get("/search", chef.EnumeratePackages)
r.Group("/cookbooks", func() {
r.Get("", chef.EnumeratePackages)
r.Post("", reqPackageAccess(perm.AccessModeWrite), chef.UploadPackage)
r.Post("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), chef.UploadPackage)
r.Group("/{name}", func() {
r.Get("", chef.PackageMetadata)
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}.json", composer.PackageMetadata)
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))
r.Group("/conan", func() {
r.Group("/v1", func() {
@ -183,14 +199,14 @@ func CommonRoutes() *web.Route {
r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeleteRecipeV1)
r.Get("/search", conan.SearchPackagesV1)
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.Group("/packages", func() {
r.Post("/delete", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV1)
r.Group("/{package_reference}", func() {
r.Get("", conan.PackageSnapshot)
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)
})
})
@ -199,11 +215,11 @@ func CommonRoutes() *web.Route {
r.Group("/files/{name}/{version}/{user}/{channel}/{recipe_revision}", func() {
r.Group("/recipe/{filename}", func() {
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.Get("", conan.DownloadPackageFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadPackageFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.UploadPackageFile)
})
}, conan.ExtractPathParameters)
})
@ -228,7 +244,7 @@ func CommonRoutes() *web.Route {
r.Get("", conan.ListRecipeRevisionFiles)
r.Group("/{filename}", func() {
r.Get("", conan.DownloadRecipeFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadRecipeFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.UploadRecipeFile)
})
})
r.Group("/packages", func() {
@ -244,7 +260,7 @@ func CommonRoutes() *web.Route {
r.Get("", conan.ListPackageRevisionFiles)
r.Group("/{filename}", func() {
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)
}
})
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) {
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), func(ctx *context.Context) {
m := uploadPattern.FindStringSubmatch(ctx.Params("*"))
if len(m) == 0 {
ctx.Status(http.StatusNotFound)
@ -301,7 +317,7 @@ func CommonRoutes() *web.Route {
r.Get("/PACKAGES{format}", cran.EnumerateSourcePackages)
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("/{platform}/contrib/{rversion}", func() {
@ -309,7 +325,7 @@ func CommonRoutes() *web.Route {
r.Get("/PACKAGES{format}", cran.EnumerateBinaryPackages)
r.Get("/{filename}", cran.DownloadBinaryPackageFile)
})
r.Put("", reqPackageAccess(perm.AccessModeWrite), cran.UploadBinaryPackageFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), cran.UploadBinaryPackageFile)
})
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/debian", func() {
@ -325,13 +341,13 @@ func CommonRoutes() *web.Route {
r.Group("/pool/{distribution}/{component}", func() {
r.Get("/{name}_{version}_{architecture}.deb", debian.DownloadPackageFile)
r.Group("", func() {
r.Put("/upload", debian.UploadPackageFile)
r.Put("/upload", enforcePackagesQuota(), debian.UploadPackageFile)
r.Delete("/{name}/{version}/{architecture}", debian.DeletePackageFile)
}, reqPackageAccess(perm.AccessModeWrite))
})
}, reqPackageAccess(perm.AccessModeRead))
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) {
ctx.Status(http.StatusNotFound)
})
@ -394,7 +410,7 @@ func CommonRoutes() *web.Route {
r.Group("/{filename}", func() {
r.Get("", generic.DownloadPackageFile)
r.Group("", func() {
r.Put("", generic.UploadPackage)
r.Put("", enforcePackagesQuota(), generic.UploadPackage)
r.Delete("", generic.DeletePackageFile)
}, reqPackageAccess(perm.AccessModeWrite))
})
@ -403,10 +419,10 @@ func CommonRoutes() *web.Route {
r.Group("/helm", func() {
r.Get("/index.yaml", helm.Index)
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))
r.Group("/maven", func() {
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile)
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), maven.UploadPackageFile)
r.Get("/*", maven.DownloadPackageFile)
r.Head("/*", maven.ProvidePackageFileHeader)
}, reqPackageAccess(perm.AccessModeRead))
@ -427,8 +443,8 @@ func CommonRoutes() *web.Route {
r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
})
r.Group("", func() {
r.Put("/", nuget.UploadPackage)
r.Put("/symbolpackage", nuget.UploadSymbolPackage)
r.Put("/", enforcePackagesQuota(), nuget.UploadPackage)
r.Put("/symbolpackage", enforcePackagesQuota(), nuget.UploadSymbolPackage)
r.Delete("/{id}/{version}", nuget.DeletePackage)
}, reqPackageAccess(perm.AccessModeWrite))
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("/@{scope}/{id}", func() {
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.Get("", npm.DownloadPackageFile)
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
@ -463,7 +479,7 @@ func CommonRoutes() *web.Route {
})
r.Group("/{id}", func() {
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.Get("", npm.DownloadPackageFile)
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
@ -496,7 +512,7 @@ func CommonRoutes() *web.Route {
r.Group("/api/packages", func() {
r.Group("/versions/new", func() {
r.Get("", pub.RequestUpload)
r.Post("/upload", pub.UploadPackageFile)
r.Post("/upload", enforcePackagesQuota(), pub.UploadPackageFile)
r.Get("/finalize/{id}/{version}", pub.FinalizePackage)
}, reqPackageAccess(perm.AccessModeWrite))
r.Group("/{id}", func() {
@ -507,7 +523,7 @@ func CommonRoutes() *web.Route {
})
}, reqPackageAccess(perm.AccessModeRead))
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("/simple/{id}", pypi.PackageMetadata)
}, reqPackageAccess(perm.AccessModeRead))
@ -556,6 +572,10 @@ func CommonRoutes() *web.Route {
if ctx.Written() {
return
}
enforcePackagesQuota()(ctx)
if ctx.Written() {
return
}
ctx.SetParams("group", strings.Trim(m[1], "/"))
rpm.UploadPackageFile(ctx)
return
@ -591,7 +611,7 @@ func CommonRoutes() *web.Route {
r.Get("/quick/Marshal.4.8/{filename}", rubygems.ServePackageSpecification)
r.Get("/gems/{filename}", rubygems.DownloadPackageFile)
r.Group("/api/v1/gems", func() {
r.Post("/", rubygems.UploadPackageFile)
r.Post("/", enforcePackagesQuota(), rubygems.UploadPackageFile)
r.Delete("/yank", rubygems.DeletePackage)
}, reqPackageAccess(perm.AccessModeWrite))
}, reqPackageAccess(perm.AccessModeRead))
@ -603,7 +623,7 @@ func CommonRoutes() *web.Route {
}, swift.CheckAcceptMediaType(swift.AcceptJSON))
r.Group("/{version}", func() {
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) {
// 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.Group("/{version}/{provider}", func() {
r.Get("", vagrant.DownloadPackageFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), vagrant.UploadPackageFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), vagrant.UploadPackageFile)
})
})
}, reqPackageAccess(perm.AccessModeRead))

View file

@ -77,6 +77,7 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
quota_model "code.gitea.io/gitea/models/quota"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@ -973,7 +974,7 @@ func Routes() *web.Route {
// (repo scope)
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)
if !setting.Repository.DisableStars {
@ -1104,7 +1105,7 @@ func Routes() *web.Route {
m.Get("", repo.ListBranches)
m.Get("/*", repo.GetBranch)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), 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))
m.Group("/branch_protections", func() {
m.Get("", repo.ListBranchProtections)
@ -1118,7 +1119,7 @@ func Routes() *web.Route {
m.Group("/tags", func() {
m.Get("", repo.ListTags)
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)
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
m.Group("/tag_protections", func() {
@ -1152,10 +1153,10 @@ func Routes() *web.Route {
m.Group("/wiki", func() {
m.Combo("/page/{pageName}").
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)
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)
}, mustEnableWiki)
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
@ -1172,15 +1173,15 @@ func Routes() *web.Route {
}, reqToken())
m.Group("/releases", func() {
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.Group("/{id}", func() {
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)
m.Group("/assets", func() {
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).
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), bind(api.EditAttachmentOptions{}), repo.EditReleaseAttachment).
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseAttachment)
@ -1192,7 +1193,7 @@ func Routes() *web.Route {
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
})
}, 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.Group("/push_mirrors", func() {
m.Combo("").Get(repo.ListPushMirrors).
@ -1211,11 +1212,11 @@ func Routes() *web.Route {
m.Combo("").Get(repo.GetPullRequest).
Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
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("/files", repo.GetPullRequestFiles)
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)
m.Group("/reviews", func() {
m.Combo("").
@ -1270,15 +1271,15 @@ func Routes() *web.Route {
m.Get("/tags/{sha}", repo.GetAnnotatedTag)
m.Get("/notes/{sha}", repo.GetNote)
}, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode))
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, 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.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.Group("/*", func() {
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.CreateFile)
m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.UpdateFile)
m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile)
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.CreateFile)
m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.UpdateFile)
m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.DeleteFile)
}, reqToken())
}, reqRepoReader(unit.TypeCode))
m.Get("/signing-key.gpg", misc.SigningKey)
@ -1335,7 +1336,7 @@ func Routes() *web.Route {
m.Group("/assets", func() {
m.Combo("").
Get(repo.ListIssueCommentAttachments).
Post(reqToken(), mustNotBeArchived, repo.CreateIssueCommentAttachment)
Post(reqToken(), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsIssues, context.QuotaTargetRepo), repo.CreateIssueCommentAttachment)
m.Combo("/{attachment_id}").
Get(repo.GetIssueCommentAttachment).
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
@ -1387,7 +1388,7 @@ func Routes() *web.Route {
m.Group("/assets", func() {
m.Combo("").
Get(repo.ListIssueAttachments).
Post(reqToken(), mustNotBeArchived, repo.CreateIssueAttachment)
Post(reqToken(), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsIssues, context.QuotaTargetRepo), repo.CreateIssueAttachment)
m.Combo("/{attachment_id}").
Get(repo.GetIssueAttachment).
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
@ -1449,7 +1450,7 @@ func Routes() *web.Route {
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
Delete(reqToken(), reqOrgOwnership(), org.Delete)
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.Get("", reqToken(), org.ListMembers)
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.
// "409":
// description: The branch with the same name already exists.
// "413":
// "$ref": "#/responses/quotaExceeded"
// "423":
// "$ref": "#/responses/repoArchivedError"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
quota_model "code.gitea.io/gitea/models/quota"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
@ -53,6 +54,8 @@ func Transfer(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
// "422":
// "$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
if opts.TeamIDs != nil {
if !newOwner.IsOrganization() {
@ -162,6 +169,8 @@ func AcceptTransfer(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/quotaExceeded"
err := acceptOrRejectRepoTransfer(ctx, true)
if ctx.Written() {
@ -233,6 +242,11 @@ func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error {
}
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)
}

View file

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

View file

@ -15,11 +15,13 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
perm_model "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
quota_model "code.gitea.io/gitea/models/quota"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
gitea_context "code.gitea.io/gitea/services/context"
pull_service "code.gitea.io/gitea/services/pull"
@ -47,6 +49,8 @@ type preReceiveContext struct {
opts *private.HookOptions
isOverQuota bool
branchName string
}
@ -140,6 +144,36 @@ func (ctx *preReceiveContext) assertPushOptions() bool {
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
func HookPreReceive(ctx *gitea_context.PrivateContext) {
opts := web.GetForm(ctx).(*private.HookOptions)
@ -156,6 +190,10 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
}
log.Trace("Git push options validation succeeded")
if err := ourCtx.checkQuota(); err != nil {
return
}
// Iterate across the provided old commit IDs
for i := range opts.OldCommitIDs {
oldCommitID := opts.OldCommitIDs[i]
@ -170,6 +208,10 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
case git.SupportProcReceive && refFullName.IsFor():
preReceiveFor(ourCtx, oldCommitID, newCommitID, refFullName)
default:
if ourCtx.isOverQuota {
ourCtx.quotaExceeded()
return
}
ourCtx.AssertCanWriteCode()
}
if ctx.Written() {
@ -211,6 +253,11 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
// Allow pushes to non-protected branches
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
}
protectBranch.Repo = repo
@ -452,6 +499,15 @@ func preReceiveTag(ctx *preReceiveContext, oldCommitID, newCommitID string, refF
})
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

View file

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models"
admin_model "code.gitea.io/gitea/models/admin"
"code.gitea.io/gitea/models/db"
quota_model "code.gitea.io/gitea/models/quota"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
@ -170,6 +171,10 @@ func MigratePost(ctx *context.Context) {
tpl := base.TplName("repo/migrate/" + form.Service.Name())
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) {
return
}
if ctx.HasError() {
ctx.HTML(http.StatusOK, tpl)
return
@ -260,6 +265,25 @@ func setMigrationContextData(ctx *context.Context, serviceType structs.GitServic
}
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 {
log.Error("Retry task failed: %v", err)
ctx.ServerError("task.RetryMigrateTask", err)

View file

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

View file

@ -17,6 +17,7 @@ import (
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
quota_model "code.gitea.io/gitea/models/quota"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@ -240,6 +241,10 @@ func CreatePost(ctx *context.Context) {
}
ctx.Data["ContextUser"] = ctxUser
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) {
return
}
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplCreate)
return
@ -363,49 +368,56 @@ func ActionTransfer(accept bool) func(ctx *context.Context) {
action = "reject_transfer"
}
err := acceptOrRejectRepoTransfer(ctx, accept)
ok, err := acceptOrRejectRepoTransfer(ctx, accept)
if err != nil {
ctx.ServerError(fmt.Sprintf("Action (%s)", action), err)
return
}
if !ok {
return
}
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)
if err != nil {
return err
return false, err
}
if err := repoTransfer.LoadAttributes(ctx); err != nil {
return err
return false, err
}
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 !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctx.Doer.ID, ctx.Doer.Name) {
return false, nil
}
if ctx.Repo.GitRepo != nil {
ctx.Repo.GitRepo.Close()
ctx.Repo.GitRepo = 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"))
} else {
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.Redirect(ctx.Repo.Repository.Link())
return nil
return true, nil
}
// RedirectDownload return a file based on the following infos:

View file

@ -17,6 +17,7 @@ import (
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
quota_model "code.gitea.io/gitea/models/quota"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@ -518,6 +519,20 @@ func SettingsPost(ctx *context.Context) {
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)
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
if ctx.Repo.GitRepo != nil {
ctx.Repo.GitRepo.Close()

View file

@ -11,6 +11,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
quota_model "code.gitea.io/gitea/models/quota"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/metrics"
@ -1196,7 +1197,7 @@ func registerRoutes(m *web.Route) {
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
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.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
@ -1244,9 +1245,9 @@ func registerRoutes(m *web.Route) {
Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
m.Combo("/_cherrypick/{sha:([a-f0-9]{4,64})}/*").Get(repo.CherryPick).
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.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)
}, repo.MustBeEditable, repo.MustBeAbleToUpload)
}, 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("/tag/*", context.RepoRefByType(context.RepoRefTag), 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("/restore", repo.RestoreBranchPost)
}, 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/download/{vTag}/{fileName}", repo.MustBeNotEmpty, repo.RedirectDownload)
m.Group("/releases", func() {
m.Get("/new", repo.NewRelease)
m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)
m.Combo("/new", context.EnforceQuotaWeb(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo)).
Get(repo.NewRelease).
Post(web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)
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)
}, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef())
m.Group("/releases", func() {
m.Get("/edit/*", repo.EditRelease)
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)
// to maintain compatibility with old attachments
@ -1410,10 +1412,10 @@ func registerRoutes(m *web.Route) {
m.Group("/wiki", func() {
m.Combo("/").
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("/*").
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}}.{ext:patch|diff}", repo.RawDiff)
}, repo.MustEnableWiki, func(ctx *context.Context) {
@ -1490,7 +1492,7 @@ func registerRoutes(m *web.Route) {
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.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("/update", repo.UpdatePullRequest)
m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits)