aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2022-11-09 07:34:27 +0100
committerGitHub <noreply@github.com>2022-11-09 14:34:27 +0800
commit20674dd05da909b42cbdd07a6682fdf1d980f011 (patch)
treef51b4a6b907380d27381705e5b2e6a1187af167b
parentcb83288530b1860677b07d72bc4ce8349e3c0d67 (diff)
downloadgitea-20674dd05da909b42cbdd07a6682fdf1d980f011.tar.gz
gitea-20674dd05da909b42cbdd07a6682fdf1d980f011.zip
Add package registry quota limits (#21584)
Related #20471 This PR adds global quota limits for the package registry. Settings for individual users/orgs can be added in a seperate PR using the settings table. Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
-rw-r--r--cmd/migrate_storage_test.go5
-rw-r--r--custom/conf/app.example.ini29
-rw-r--r--docs/content/doc/advanced/config-cheat-sheet.en-us.md14
-rw-r--r--models/packages/package_file.go10
-rw-r--r--models/packages/package_version.go9
-rw-r--r--modules/setting/packages.go50
-rw-r--r--modules/setting/packages_test.go31
-rw-r--r--routers/api/packages/composer/composer.go14
-rw-r--r--routers/api/packages/conan/conan.go14
-rw-r--r--routers/api/packages/generic/generic.go14
-rw-r--r--routers/api/packages/helm/helm.go10
-rw-r--r--routers/api/packages/maven/maven.go10
-rw-r--r--routers/api/packages/npm/npm.go14
-rw-r--r--routers/api/packages/nuget/nuget.go28
-rw-r--r--routers/api/packages/pub/pub.go14
-rw-r--r--routers/api/packages/pypi/pypi.go14
-rw-r--r--routers/api/packages/rubygems/rubygems.go14
-rw-r--r--routers/api/packages/vagrant/vagrant.go14
-rw-r--r--services/packages/packages.go97
-rw-r--r--tests/integration/api_packages_test.go34
20 files changed, 378 insertions, 61 deletions
diff --git a/cmd/migrate_storage_test.go b/cmd/migrate_storage_test.go
index 0d264ef5a1..7051591ad6 100644
--- a/cmd/migrate_storage_test.go
+++ b/cmd/migrate_storage_test.go
@@ -44,8 +44,9 @@ func TestMigratePackages(t *testing.T) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: "a.go",
},
- Data: buf,
- IsLead: true,
+ Creator: creator,
+ Data: buf,
+ IsLead: true,
})
assert.NoError(t, err)
assert.NotNil(t, v)
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index b59ceee4f1..b46dfc20a9 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -2335,6 +2335,35 @@ ROUTER = console
;;
;; Path for chunked uploads. Defaults to APP_DATA_PATH + `tmp/package-upload`
;CHUNKED_UPLOAD_PATH = tmp/package-upload
+;;
+;; Maxmimum count of package versions a single owner can have (`-1` means no limits)
+;LIMIT_TOTAL_OWNER_COUNT = -1
+;; Maxmimum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_TOTAL_OWNER_SIZE = -1
+;; Maxmimum size of a Composer upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_COMPOSER = -1
+;; Maxmimum size of a Conan upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_CONAN = -1
+;; Maxmimum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_CONTAINER = -1
+;; Maxmimum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_GENERIC = -1
+;; Maxmimum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_HELM = -1
+;; Maxmimum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_MAVEN = -1
+;; Maxmimum size of a npm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_NPM = -1
+;; Maxmimum size of a NuGet upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_NUGET = -1
+;; Maxmimum size of a Pub upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_PUB = -1
+;; Maxmimum size of a PyPI upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_PYPI = -1
+;; Maxmimum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_RUBYGEMS = -1
+;; Maxmimum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_VAGRANT = -1
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index df1911934c..28bcaf29af 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -1138,6 +1138,20 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
- `ENABLED`: **true**: Enable/Disable package registry capabilities
- `CHUNKED_UPLOAD_PATH`: **tmp/package-upload**: Path for chunked uploads. Defaults to `APP_DATA_PATH` + `tmp/package-upload`
+- `LIMIT_TOTAL_OWNER_COUNT`: **-1**: Maxmimum count of package versions a single owner can have (`-1` means no limits)
+- `LIMIT_TOTAL_OWNER_SIZE`: **-1**: Maxmimum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_COMPOSER`: **-1**: Maxmimum size of a Composer upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_CONAN`: **-1**: Maxmimum size of a Conan upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_CONTAINER`: **-1**: Maxmimum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_GENERIC`: **-1**: Maxmimum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_HELM`: **-1**: Maxmimum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_MAVEN`: **-1**: Maxmimum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_NPM`: **-1**: Maxmimum size of a npm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_NUGET`: **-1**: Maxmimum size of a NuGet upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_PUB`: **-1**: Maxmimum size of a Pub upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_PYPI`: **-1**: Maxmimum size of a PyPI upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_RUBYGEMS`: **-1**: Maxmimum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_VAGRANT`: **-1**: Maxmimum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
## Mirror (`mirror`)
diff --git a/models/packages/package_file.go b/models/packages/package_file.go
index 8f304ce8ac..9f6284af07 100644
--- a/models/packages/package_file.go
+++ b/models/packages/package_file.go
@@ -199,3 +199,13 @@ func SearchFiles(ctx context.Context, opts *PackageFileSearchOptions) ([]*Packag
count, err := sess.FindAndCount(&pfs)
return pfs, count, err
}
+
+// CalculateBlobSize sums up all blob sizes matching the search options.
+// It does NOT respect the deduplication of blobs.
+func CalculateBlobSize(ctx context.Context, opts *PackageFileSearchOptions) (int64, error) {
+ return db.GetEngine(ctx).
+ Table("package_file").
+ Where(opts.toConds()).
+ Join("INNER", "package_blob", "package_blob.id = package_file.blob_id").
+ SumInt(new(PackageBlob), "size")
+}
diff --git a/models/packages/package_version.go b/models/packages/package_version.go
index 782261c575..48c6aa7d60 100644
--- a/models/packages/package_version.go
+++ b/models/packages/package_version.go
@@ -319,3 +319,12 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
count, err := sess.FindAndCount(&pvs)
return pvs, count, err
}
+
+// CountVersions counts all versions of packages matching the search options
+func CountVersions(ctx context.Context, opts *PackageSearchOptions) (int64, error) {
+ return db.GetEngine(ctx).
+ Where(opts.toConds()).
+ Table("package_version").
+ Join("INNER", "package", "package.id = package_version.package_id").
+ Count(new(PackageVersion))
+}
diff --git a/modules/setting/packages.go b/modules/setting/packages.go
index 5e0f2a3b03..62201032c7 100644
--- a/modules/setting/packages.go
+++ b/modules/setting/packages.go
@@ -5,11 +5,15 @@
package setting
import (
+ "math"
"net/url"
"os"
"path/filepath"
"code.gitea.io/gitea/modules/log"
+
+ "github.com/dustin/go-humanize"
+ ini "gopkg.in/ini.v1"
)
// Package registry settings
@@ -19,8 +23,24 @@ var (
Enabled bool
ChunkedUploadPath string
RegistryHost string
+
+ LimitTotalOwnerCount int64
+ LimitTotalOwnerSize int64
+ LimitSizeComposer int64
+ LimitSizeConan int64
+ LimitSizeContainer int64
+ LimitSizeGeneric int64
+ LimitSizeHelm int64
+ LimitSizeMaven int64
+ LimitSizeNpm int64
+ LimitSizeNuGet int64
+ LimitSizePub int64
+ LimitSizePyPI int64
+ LimitSizeRubyGems int64
+ LimitSizeVagrant int64
}{
- Enabled: true,
+ Enabled: true,
+ LimitTotalOwnerCount: -1,
}
)
@@ -43,4 +63,32 @@ func newPackages() {
if err := os.MkdirAll(Packages.ChunkedUploadPath, os.ModePerm); err != nil {
log.Error("Unable to create chunked upload directory: %s (%v)", Packages.ChunkedUploadPath, err)
}
+
+ Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
+ Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER")
+ Packages.LimitSizeConan = mustBytes(sec, "LIMIT_SIZE_CONAN")
+ Packages.LimitSizeContainer = mustBytes(sec, "LIMIT_SIZE_CONTAINER")
+ Packages.LimitSizeGeneric = mustBytes(sec, "LIMIT_SIZE_GENERIC")
+ Packages.LimitSizeHelm = mustBytes(sec, "LIMIT_SIZE_HELM")
+ Packages.LimitSizeMaven = mustBytes(sec, "LIMIT_SIZE_MAVEN")
+ Packages.LimitSizeNpm = mustBytes(sec, "LIMIT_SIZE_NPM")
+ Packages.LimitSizeNuGet = mustBytes(sec, "LIMIT_SIZE_NUGET")
+ Packages.LimitSizePub = mustBytes(sec, "LIMIT_SIZE_PUB")
+ Packages.LimitSizePyPI = mustBytes(sec, "LIMIT_SIZE_PYPI")
+ Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
+ Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
+}
+
+func mustBytes(section *ini.Section, key string) int64 {
+ const noLimit = "-1"
+
+ value := section.Key(key).MustString(noLimit)
+ if value == noLimit {
+ return -1
+ }
+ bytes, err := humanize.ParseBytes(value)
+ if err != nil || bytes > math.MaxInt64 {
+ return -1
+ }
+ return int64(bytes)
}
diff --git a/modules/setting/packages_test.go b/modules/setting/packages_test.go
new file mode 100644
index 0000000000..059273dce4
--- /dev/null
+++ b/modules/setting/packages_test.go
@@ -0,0 +1,31 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package setting
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ ini "gopkg.in/ini.v1"
+)
+
+func TestMustBytes(t *testing.T) {
+ test := func(value string) int64 {
+ sec, _ := ini.Empty().NewSection("test")
+ sec.NewKey("VALUE", value)
+
+ return mustBytes(sec, "VALUE")
+ }
+
+ assert.EqualValues(t, -1, test(""))
+ assert.EqualValues(t, -1, test("-1"))
+ assert.EqualValues(t, 0, test("0"))
+ assert.EqualValues(t, 1, test("1"))
+ assert.EqualValues(t, 10000, test("10000"))
+ assert.EqualValues(t, 1000000, test("1 mb"))
+ assert.EqualValues(t, 1048576, test("1mib"))
+ assert.EqualValues(t, 1782579, test("1.7mib"))
+ assert.EqualValues(t, -1, test("1 yib")) // too large
+}
diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go
index 86ef7cbd9a..92e83dbe79 100644
--- a/routers/api/packages/composer/composer.go
+++ b/routers/api/packages/composer/composer.go
@@ -235,16 +235,20 @@ func UploadPackage(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)),
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageVersion {
+ switch err {
+ case packages_model.ErrDuplicatePackageVersion:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go
index dd078d6ad3..c8c9dc3e38 100644
--- a/routers/api/packages/conan/conan.go
+++ b/routers/api/packages/conan/conan.go
@@ -348,8 +348,9 @@ func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey
Filename: strings.ToLower(filename),
CompositeKey: fileKey,
},
- Data: buf,
- IsLead: isConanfileFile,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: isConanfileFile,
Properties: map[string]string{
conan_module.PropertyRecipeUser: rref.User,
conan_module.PropertyRecipeChannel: rref.Channel,
@@ -416,11 +417,14 @@ func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey
pfci,
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageFile {
+ switch err {
+ case packages_model.ErrDuplicatePackageFile:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go
index 81891bec26..1bccc6764c 100644
--- a/routers/api/packages/generic/generic.go
+++ b/routers/api/packages/generic/generic.go
@@ -104,16 +104,20 @@ func UploadPackage(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: filename,
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageFile {
+ switch err {
+ case packages_model.ErrDuplicatePackageFile:
apiError(ctx, http.StatusConflict, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go
index 9c85e0874f..662d9a5dda 100644
--- a/routers/api/packages/helm/helm.go
+++ b/routers/api/packages/helm/helm.go
@@ -186,17 +186,21 @@ func UploadPackage(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: createFilename(metadata),
},
+ Creator: ctx.Doer,
Data: buf,
IsLead: true,
OverwriteExisting: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageVersion {
+ switch err {
+ case packages_model.ErrDuplicatePackageVersion:
apiError(ctx, http.StatusConflict, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index bf00c199f5..de274b2046 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -266,6 +266,7 @@ func UploadPackageFile(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: params.Filename,
},
+ Creator: ctx.Doer,
Data: buf,
IsLead: false,
OverwriteExisting: params.IsMeta,
@@ -312,11 +313,14 @@ func UploadPackageFile(ctx *context.Context) {
pfci,
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageFile {
+ switch err {
+ case packages_model.ErrDuplicatePackageFile:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go
index 82dae0cf43..6d589bde3a 100644
--- a/routers/api/packages/npm/npm.go
+++ b/routers/api/packages/npm/npm.go
@@ -180,16 +180,20 @@ func UploadPackage(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: npmPackage.Filename,
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageVersion {
+ switch err {
+ case packages_model.ErrDuplicatePackageVersion:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go
index e84aef3160..442d94243b 100644
--- a/routers/api/packages/nuget/nuget.go
+++ b/routers/api/packages/nuget/nuget.go
@@ -374,16 +374,20 @@ func UploadPackage(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: strings.ToLower(fmt.Sprintf("%s.%s.nupkg", np.ID, np.Version)),
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageVersion {
+ switch err {
+ case packages_model.ErrDuplicatePackageVersion:
apiError(ctx, http.StatusConflict, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
@@ -428,8 +432,9 @@ func UploadSymbolPackage(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: strings.ToLower(fmt.Sprintf("%s.%s.snupkg", np.ID, np.Version)),
},
- Data: buf,
- IsLead: false,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: false,
},
)
if err != nil {
@@ -438,6 +443,8 @@ func UploadSymbolPackage(ctx *context.Context) {
apiError(ctx, http.StatusNotFound, err)
case packages_model.ErrDuplicatePackageFile:
apiError(ctx, http.StatusConflict, err)
+ case packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
@@ -452,8 +459,9 @@ func UploadSymbolPackage(ctx *context.Context) {
Filename: strings.ToLower(pdb.Name),
CompositeKey: strings.ToLower(pdb.ID),
},
- Data: pdb.Content,
- IsLead: false,
+ Creator: ctx.Doer,
+ Data: pdb.Content,
+ IsLead: false,
Properties: map[string]string{
nuget_module.PropertySymbolID: strings.ToLower(pdb.ID),
},
@@ -463,6 +471,8 @@ func UploadSymbolPackage(ctx *context.Context) {
switch err {
case packages_model.ErrDuplicatePackageFile:
apiError(ctx, http.StatusConflict, err)
+ case packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
diff --git a/routers/api/packages/pub/pub.go b/routers/api/packages/pub/pub.go
index 9af0ceeb0e..635147b6d0 100644
--- a/routers/api/packages/pub/pub.go
+++ b/routers/api/packages/pub/pub.go
@@ -199,16 +199,20 @@ func UploadPackageFile(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: strings.ToLower(pck.Version + ".tar.gz"),
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageVersion {
+ switch err {
+ case packages_model.ErrDuplicatePackageVersion:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go
index 4c8041c30c..4853e6658b 100644
--- a/routers/api/packages/pypi/pypi.go
+++ b/routers/api/packages/pypi/pypi.go
@@ -162,16 +162,20 @@ func UploadPackageFile(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: fileHeader.Filename,
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageFile {
+ switch err {
+ case packages_model.ErrDuplicatePackageFile:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go
index 319c94b91f..eeae21146c 100644
--- a/routers/api/packages/rubygems/rubygems.go
+++ b/routers/api/packages/rubygems/rubygems.go
@@ -242,16 +242,20 @@ func UploadPackageFile(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: filename,
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageVersion {
+ switch err {
+ case packages_model.ErrDuplicatePackageVersion:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go
index 7750e5dc4b..31ac56a532 100644
--- a/routers/api/packages/vagrant/vagrant.go
+++ b/routers/api/packages/vagrant/vagrant.go
@@ -193,19 +193,23 @@ func UploadPackageFile(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: strings.ToLower(boxProvider),
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
Properties: map[string]string{
vagrant_module.PropertyProvider: strings.TrimSuffix(boxProvider, ".box"),
},
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageFile {
+ switch err {
+ case packages_model.ErrDuplicatePackageFile:
apiError(ctx, http.StatusConflict, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
diff --git a/services/packages/packages.go b/services/packages/packages.go
index 96132eac09..443976e174 100644
--- a/services/packages/packages.go
+++ b/services/packages/packages.go
@@ -6,6 +6,7 @@ package packages
import (
"context"
+ "errors"
"fmt"
"io"
"strings"
@@ -19,10 +20,17 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
packages_module "code.gitea.io/gitea/modules/packages"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
container_service "code.gitea.io/gitea/services/packages/container"
)
+var (
+ ErrQuotaTypeSize = errors.New("maximum allowed package type size exceeded")
+ ErrQuotaTotalSize = errors.New("maximum allowed package storage quota exceeded")
+ ErrQuotaTotalCount = errors.New("maximum allowed package count exceeded")
+)
+
// PackageInfo describes a package
type PackageInfo struct {
Owner *user_model.User
@@ -50,6 +58,7 @@ type PackageFileInfo struct {
// PackageFileCreationInfo describes a package file to create
type PackageFileCreationInfo struct {
PackageFileInfo
+ Creator *user_model.User
Data packages_module.HashedSizeReader
IsLead bool
Properties map[string]string
@@ -78,7 +87,7 @@ func createPackageAndAddFile(pvci *PackageCreationInfo, pfci *PackageFileCreatio
return nil, nil, err
}
- pf, pb, blobCreated, err := addFileToPackageVersion(ctx, pv, pfci)
+ pf, pb, blobCreated, err := addFileToPackageVersion(ctx, pv, &pvci.PackageInfo, pfci)
removeBlob := false
defer func() {
if blobCreated && removeBlob {
@@ -164,6 +173,10 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
}
if versionCreated {
+ if err := checkCountQuotaExceeded(ctx, pvci.Creator, pvci.Owner); err != nil {
+ return nil, false, err
+ }
+
for name, value := range pvci.VersionProperties {
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, name, value); err != nil {
log.Error("Error setting package version property: %v", err)
@@ -188,7 +201,7 @@ func AddFileToExistingPackage(pvi *PackageInfo, pfci *PackageFileCreationInfo) (
return nil, nil, err
}
- pf, pb, blobCreated, err := addFileToPackageVersion(ctx, pv, pfci)
+ pf, pb, blobCreated, err := addFileToPackageVersion(ctx, pv, pvi, pfci)
removeBlob := false
defer func() {
if removeBlob {
@@ -224,9 +237,13 @@ func NewPackageBlob(hsr packages_module.HashedSizeReader) *packages_model.Packag
}
}
-func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
+func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pvi *PackageInfo, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
log.Trace("Adding package file: %v, %s", pv.ID, pfci.Filename)
+ if err := checkSizeQuotaExceeded(ctx, pfci.Creator, pvi.Owner, pvi.PackageType, pfci.Data.Size()); err != nil {
+ return nil, nil, false, err
+ }
+
pb, exists, err := packages_model.GetOrInsertBlob(ctx, NewPackageBlob(pfci.Data))
if err != nil {
log.Error("Error inserting package blob: %v", err)
@@ -285,6 +302,80 @@ func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVers
return pf, pb, !exists, nil
}
+func checkCountQuotaExceeded(ctx context.Context, doer, owner *user_model.User) error {
+ if doer.IsAdmin {
+ return nil
+ }
+
+ if setting.Packages.LimitTotalOwnerCount > -1 {
+ totalCount, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
+ OwnerID: owner.ID,
+ IsInternal: util.OptionalBoolFalse,
+ })
+ if err != nil {
+ log.Error("CountVersions failed: %v", err)
+ return err
+ }
+ if totalCount > setting.Packages.LimitTotalOwnerCount {
+ return ErrQuotaTotalCount
+ }
+ }
+
+ return nil
+}
+
+func checkSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, packageType packages_model.Type, uploadSize int64) error {
+ if doer.IsAdmin {
+ return nil
+ }
+
+ var typeSpecificSize int64
+ switch packageType {
+ case packages_model.TypeComposer:
+ typeSpecificSize = setting.Packages.LimitSizeComposer
+ case packages_model.TypeConan:
+ typeSpecificSize = setting.Packages.LimitSizeConan
+ case packages_model.TypeContainer:
+ typeSpecificSize = setting.Packages.LimitSizeContainer
+ case packages_model.TypeGeneric:
+ typeSpecificSize = setting.Packages.LimitSizeGeneric
+ case packages_model.TypeHelm:
+ typeSpecificSize = setting.Packages.LimitSizeHelm
+ case packages_model.TypeMaven:
+ typeSpecificSize = setting.Packages.LimitSizeMaven
+ case packages_model.TypeNpm:
+ typeSpecificSize = setting.Packages.LimitSizeNpm
+ case packages_model.TypeNuGet:
+ typeSpecificSize = setting.Packages.LimitSizeNuGet
+ case packages_model.TypePub:
+ typeSpecificSize = setting.Packages.LimitSizePub
+ case packages_model.TypePyPI:
+ typeSpecificSize = setting.Packages.LimitSizePyPI
+ case packages_model.TypeRubyGems:
+ typeSpecificSize = setting.Packages.LimitSizeRubyGems
+ case packages_model.TypeVagrant:
+ typeSpecificSize = setting.Packages.LimitSizeVagrant
+ }
+ if typeSpecificSize > -1 && typeSpecificSize < uploadSize {
+ return ErrQuotaTypeSize
+ }
+
+ if setting.Packages.LimitTotalOwnerSize > -1 {
+ totalSize, err := packages_model.CalculateBlobSize(ctx, &packages_model.PackageFileSearchOptions{
+ OwnerID: owner.ID,
+ })
+ if err != nil {
+ log.Error("CalculateBlobSize failed: %v", err)
+ return err
+ }
+ if totalSize+uploadSize > setting.Packages.LimitTotalOwnerSize {
+ return ErrQuotaTotalSize
+ }
+ }
+
+ return nil
+}
+
// RemovePackageVersionByNameAndVersion deletes a package version and all associated files
func RemovePackageVersionByNameAndVersion(doer *user_model.User, pvi *PackageInfo) error {
pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version)
diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go
index 25f5b3f2a1..815685ea79 100644
--- a/tests/integration/api_packages_test.go
+++ b/tests/integration/api_packages_test.go
@@ -16,6 +16,7 @@ import (
container_model "code.gitea.io/gitea/models/packages/container"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
packages_service "code.gitea.io/gitea/services/packages"
"code.gitea.io/gitea/tests"
@@ -166,6 +167,39 @@ func TestPackageAccess(t *testing.T) {
uploadPackage(admin, user, http.StatusCreated)
}
+func TestPackageQuota(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ limitTotalOwnerCount, limitTotalOwnerSize, limitSizeGeneric := setting.Packages.LimitTotalOwnerCount, setting.Packages.LimitTotalOwnerSize, setting.Packages.LimitSizeGeneric
+
+ admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
+
+ uploadPackage := func(doer *user_model.User, version string, expectedStatus int) {
+ url := fmt.Sprintf("/api/packages/%s/generic/test-package/%s/file.bin", user.Name, version)
+ req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{1}))
+ AddBasicAuthHeader(req, doer.Name)
+ MakeRequest(t, req, expectedStatus)
+ }
+
+ // Exceeded quota result in StatusForbidden for normal users but admins are always allowed to upload.
+
+ setting.Packages.LimitTotalOwnerCount = 0
+ uploadPackage(user, "1.0", http.StatusForbidden)
+ uploadPackage(admin, "1.0", http.StatusCreated)
+ setting.Packages.LimitTotalOwnerCount = limitTotalOwnerCount
+
+ setting.Packages.LimitTotalOwnerSize = 0
+ uploadPackage(user, "1.1", http.StatusForbidden)
+ uploadPackage(admin, "1.1", http.StatusCreated)
+ setting.Packages.LimitTotalOwnerSize = limitTotalOwnerSize
+
+ setting.Packages.LimitSizeGeneric = 0
+ uploadPackage(user, "1.2", http.StatusForbidden)
+ uploadPackage(admin, "1.2", http.StatusCreated)
+ setting.Packages.LimitSizeGeneric = limitSizeGeneric
+}
+
func TestPackageCleanup(t *testing.T) {
defer tests.PrepareTestEnv(t)()