feat(repo): split repository creation limit into user and org scopes (#37872)

## Background

`MAX_CREATION_LIMIT` applies to whoever owns a new repository, with no
distinction between individual users and organizations. Admins who want
different limits for the two - most commonly "block personal repos but
let orgs create freely" - currently have to set per-user / per-org
overrides on every entity.

## Changes

Adds two new `[repository]` settings:

- `USER_MAX_CREATION_LIMIT`: global limit for individual users
- `ORG_MAX_CREATION_LIMIT`: global limit for organizations

`MAX_CREATION_LIMIT` is kept as a shortcut: when set, it becomes the
default value for both new keys. When the new keys are explicitly
configured, they take precedence. Deployments that only set
`MAX_CREATION_LIMIT` see behavior identical to now.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
Zettat123
2026-05-28 11:29:32 -06:00
committed by GitHub
parent 52fef74291
commit 49f88a4b9e
7 changed files with 137 additions and 18 deletions
+38 -2
View File
@@ -674,12 +674,18 @@ func TestGetInactiveUsers(t *testing.T) {
func TestCanCreateRepo(t *testing.T) {
defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)()
defer test.MockVariableValue(&setting.Repository.UserMaxCreationLimit)()
defer test.MockVariableValue(&setting.Repository.OrgMaxCreationLimit)()
const noLimit = -1
doerActions := user_model.NewActionsUser()
doerNormal := &user_model.User{ID: 2}
doerAdmin := &user_model.User{ID: 1, IsAdmin: true}
orgOwner := func(numRepos, maxRepoCreation int) *user_model.User {
return &user_model.User{ID: 3, Type: user_model.UserTypeOrganization, NumRepos: numRepos, MaxRepoCreation: maxRepoCreation}
}
t.Run("NoGlobalLimit", func(t *testing.T) {
setting.Repository.MaxCreationLimit = noLimit
setting.Repository.UserMaxCreationLimit = noLimit
setting.Repository.OrgMaxCreationLimit = noLimit
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
@@ -693,7 +699,8 @@ func TestCanCreateRepo(t *testing.T) {
})
t.Run("GlobalLimit50", func(t *testing.T) {
setting.Repository.MaxCreationLimit = 50
setting.Repository.UserMaxCreationLimit = 50
setting.Repository.OrgMaxCreationLimit = 50
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit
@@ -707,4 +714,33 @@ func TestCanCreateRepo(t *testing.T) {
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100}))
})
t.Run("UserBlockedOrgsUnlimited", func(t *testing.T) {
// User and org limits are independent: a deployment can block personal repos while leaving orgs unrestricted.
setting.Repository.UserMaxCreationLimit = 0
setting.Repository.OrgMaxCreationLimit = noLimit
// regular user is blocked
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 0, MaxRepoCreation: noLimit}))
// per-user override grants individual exceptions even when the global user limit is 0
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 3, MaxRepoCreation: 5}))
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 5, MaxRepoCreation: 5}))
// organization can create unlimited repos
assert.True(t, doerNormal.CanCreateRepoIn(orgOwner(10, noLimit)))
assert.True(t, doerNormal.CanCreateRepoIn(orgOwner(999, noLimit)))
// per-org override still wins over the global org limit
assert.False(t, doerNormal.CanCreateRepoIn(orgOwner(5, 5)))
})
t.Run("OrgGlobalLimitWithPerOrgOverride", func(t *testing.T) {
setting.Repository.UserMaxCreationLimit = noLimit
setting.Repository.OrgMaxCreationLimit = 10
assert.True(t, doerNormal.CanCreateRepoIn(orgOwner(5, noLimit)))
assert.False(t, doerNormal.CanCreateRepoIn(orgOwner(10, noLimit)))
// per-org override bypasses the global org limit
assert.True(t, doerNormal.CanCreateRepoIn(orgOwner(10, 100)))
})
}