mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-29 18:38:28 +00:00 
			
		
		
		
	fix users being able bypass limits with repo transfers (#34031)
prevent user from being able to transfer repo to user who cannot have more repositories
This commit is contained in:
		| @@ -21,3 +21,11 @@ | |||||||
|   repo_id: 32 |   repo_id: 32 | ||||||
|   created_unix: 1553610671 |   created_unix: 1553610671 | ||||||
|   updated_unix: 1553610671 |   updated_unix: 1553610671 | ||||||
|  |  | ||||||
|  | - | ||||||
|  |   id: 4 | ||||||
|  |   doer_id: 3 | ||||||
|  |   recipient_id: 1 | ||||||
|  |   repo_id: 5 | ||||||
|  |   created_unix: 1553610671 | ||||||
|  |   updated_unix: 1553610671 | ||||||
|   | |||||||
| @@ -108,22 +108,19 @@ func Transfer(ctx *context.APIContext) { | |||||||
| 	oldFullname := ctx.Repo.Repository.FullName() | 	oldFullname := ctx.Repo.Repository.FullName() | ||||||
|  |  | ||||||
| 	if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil { | 	if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil { | ||||||
| 		if repo_model.IsErrRepoTransferInProgress(err) { | 		switch { | ||||||
|  | 		case repo_model.IsErrRepoTransferInProgress(err): | ||||||
| 			ctx.APIError(http.StatusConflict, err) | 			ctx.APIError(http.StatusConflict, err) | ||||||
| 			return | 		case repo_model.IsErrRepoAlreadyExist(err): | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if repo_model.IsErrRepoAlreadyExist(err) { |  | ||||||
| 			ctx.APIError(http.StatusUnprocessableEntity, err) | 			ctx.APIError(http.StatusUnprocessableEntity, err) | ||||||
|  | 		case repo_service.IsRepositoryLimitReached(err): | ||||||
|  | 			ctx.APIError(http.StatusForbidden, err) | ||||||
|  | 		case errors.Is(err, user_model.ErrBlockedUser): | ||||||
|  | 			ctx.APIError(http.StatusForbidden, err) | ||||||
|  | 		default: | ||||||
|  | 			ctx.APIErrorInternal(err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if errors.Is(err, user_model.ErrBlockedUser) { |  | ||||||
| 			ctx.APIError(http.StatusForbidden, err) |  | ||||||
| 		} else { |  | ||||||
| 			ctx.APIErrorInternal(err) |  | ||||||
| 		} |  | ||||||
| 		return |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer { | 	if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer { | ||||||
| @@ -169,6 +166,8 @@ func AcceptTransfer(ctx *context.APIContext) { | |||||||
| 			ctx.APIError(http.StatusNotFound, err) | 			ctx.APIError(http.StatusNotFound, err) | ||||||
| 		case errors.Is(err, util.ErrPermissionDenied): | 		case errors.Is(err, util.ErrPermissionDenied): | ||||||
| 			ctx.APIError(http.StatusForbidden, err) | 			ctx.APIError(http.StatusForbidden, err) | ||||||
|  | 		case repo_service.IsRepositoryLimitReached(err): | ||||||
|  | 			ctx.APIError(http.StatusForbidden, err) | ||||||
| 		default: | 		default: | ||||||
| 			ctx.APIErrorInternal(err) | 			ctx.APIErrorInternal(err) | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -305,11 +305,15 @@ func CreatePost(ctx *context.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func handleActionError(ctx *context.Context, err error) { | func handleActionError(ctx *context.Context, err error) { | ||||||
| 	if errors.Is(err, user_model.ErrBlockedUser) { | 	switch { | ||||||
|  | 	case errors.Is(err, user_model.ErrBlockedUser): | ||||||
| 		ctx.Flash.Error(ctx.Tr("repo.action.blocked_user")) | 		ctx.Flash.Error(ctx.Tr("repo.action.blocked_user")) | ||||||
| 	} else if errors.Is(err, util.ErrPermissionDenied) { | 	case repo_service.IsRepositoryLimitReached(err): | ||||||
|  | 		limit := err.(repo_service.LimitReachedError).Limit | ||||||
|  | 		ctx.Flash.Error(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit)) | ||||||
|  | 	case errors.Is(err, util.ErrPermissionDenied): | ||||||
| 		ctx.HTTPError(http.StatusNotFound) | 		ctx.HTTPError(http.StatusNotFound) | ||||||
| 	} else { | 	default: | ||||||
| 		ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err) | 		ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -848,6 +848,9 @@ func handleSettingsPostTransfer(ctx *context.Context) { | |||||||
| 			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) | 			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) | ||||||
| 		} else if repo_model.IsErrRepoTransferInProgress(err) { | 		} else if repo_model.IsErrRepoTransferInProgress(err) { | ||||||
| 			ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) | 			ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) | ||||||
|  | 		} else if repo_service.IsRepositoryLimitReached(err) { | ||||||
|  | 			limit := err.(repo_service.LimitReachedError).Limit | ||||||
|  | 			ctx.RenderWithErr(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit), tplSettingsOptions, nil) | ||||||
| 		} else if errors.Is(err, user_model.ErrBlockedUser) { | 		} else if errors.Is(err, user_model.ErrBlockedUser) { | ||||||
| 			ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) | 			ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) | ||||||
| 		} else { | 		} else { | ||||||
|   | |||||||
| @@ -20,10 +20,22 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/gitrepo" | 	"code.gitea.io/gitea/modules/gitrepo" | ||||||
| 	"code.gitea.io/gitea/modules/globallock" | 	"code.gitea.io/gitea/modules/globallock" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	notify_service "code.gitea.io/gitea/services/notify" | 	notify_service "code.gitea.io/gitea/services/notify" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | type LimitReachedError struct{ Limit int } | ||||||
|  |  | ||||||
|  | func (LimitReachedError) Error() string { | ||||||
|  | 	return "Repository limit has been reached" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func IsRepositoryLimitReached(err error) bool { | ||||||
|  | 	_, ok := err.(LimitReachedError) | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
|  |  | ||||||
| func getRepoWorkingLockKey(repoID int64) string { | func getRepoWorkingLockKey(repoID int64) string { | ||||||
| 	return fmt.Sprintf("repo_working_%d", repoID) | 	return fmt.Sprintf("repo_working_%d", repoID) | ||||||
| } | } | ||||||
| @@ -49,6 +61,11 @@ func AcceptTransferOwnership(ctx context.Context, repo *repo_model.Repository, d | |||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if !doer.IsAdmin && !repoTransfer.Recipient.CanCreateRepo() { | ||||||
|  | 			limit := util.Iif(repoTransfer.Recipient.MaxRepoCreation >= 0, repoTransfer.Recipient.MaxRepoCreation, setting.Repository.MaxCreationLimit) | ||||||
|  | 			return LimitReachedError{Limit: limit} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if !repoTransfer.CanUserAcceptOrRejectTransfer(ctx, doer) { | 		if !repoTransfer.CanUserAcceptOrRejectTransfer(ctx, doer) { | ||||||
| 			return util.ErrPermissionDenied | 			return util.ErrPermissionDenied | ||||||
| 		} | 		} | ||||||
| @@ -399,6 +416,11 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if !doer.IsAdmin && !newOwner.CanCreateRepo() { | ||||||
|  | 		limit := util.Iif(newOwner.MaxRepoCreation >= 0, newOwner.MaxRepoCreation, setting.Repository.MaxCreationLimit) | ||||||
|  | 		return LimitReachedError{Limit: limit} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	var isDirectTransfer bool | 	var isDirectTransfer bool | ||||||
| 	oldOwnerName := repo.OwnerName | 	oldOwnerName := repo.OwnerName | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| // Copyright 2019 The Gitea Authors. All rights reserved. | // Copyright 2025 The Gitea Authors. All rights reserved. | ||||||
| // SPDX-License-Identifier: MIT | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
| package repository | package repository | ||||||
| @@ -14,11 +14,14 @@ import ( | |||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/test" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	"code.gitea.io/gitea/services/feed" | 	"code.gitea.io/gitea/services/feed" | ||||||
| 	notify_service "code.gitea.io/gitea/services/notify" | 	notify_service "code.gitea.io/gitea/services/notify" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"github.com/stretchr/testify/require" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var notifySync sync.Once | var notifySync sync.Once | ||||||
| @@ -125,3 +128,40 @@ func TestRepositoryTransfer(t *testing.T) { | |||||||
| 	err = RejectRepositoryTransfer(db.DefaultContext, repo2, doer) | 	err = RejectRepositoryTransfer(db.DefaultContext, repo2, doer) | ||||||
| 	assert.True(t, repo_model.IsErrNoPendingTransfer(err)) | 	assert.True(t, repo_model.IsErrNoPendingTransfer(err)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Test transfer rejections | ||||||
|  | func TestRepositoryTransferRejection(t *testing.T) { | ||||||
|  | 	require.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  | 	// Set limit to 0 repositories so no repositories can be transferred | ||||||
|  | 	defer test.MockVariableValue(&setting.Repository.MaxCreationLimit, 0)() | ||||||
|  |  | ||||||
|  | 	// Admin case | ||||||
|  | 	doerAdmin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) | ||||||
|  | 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 5}) | ||||||
|  |  | ||||||
|  | 	transfer, err := repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) | ||||||
|  | 	require.NoError(t, err) | ||||||
|  | 	require.NotNil(t, transfer) | ||||||
|  | 	require.NoError(t, transfer.LoadRecipient(db.DefaultContext)) | ||||||
|  |  | ||||||
|  | 	require.True(t, transfer.Recipient.CanCreateRepo()) // admin is not subject to limits | ||||||
|  |  | ||||||
|  | 	// Administrator should not be affected by the limits so transfer should be successful | ||||||
|  | 	assert.NoError(t, AcceptTransferOwnership(db.DefaultContext, repo, doerAdmin)) | ||||||
|  |  | ||||||
|  | 	// Non admin user case | ||||||
|  | 	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10}) | ||||||
|  | 	repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21}) | ||||||
|  |  | ||||||
|  | 	transfer, err = repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) | ||||||
|  | 	require.NoError(t, err) | ||||||
|  | 	require.NotNil(t, transfer) | ||||||
|  | 	require.NoError(t, transfer.LoadRecipient(db.DefaultContext)) | ||||||
|  |  | ||||||
|  | 	require.False(t, transfer.Recipient.CanCreateRepo()) // regular user is subject to limits | ||||||
|  |  | ||||||
|  | 	// Cannot accept because of the limit | ||||||
|  | 	err = AcceptTransferOwnership(db.DefaultContext, repo, doer) | ||||||
|  | 	assert.Error(t, err) | ||||||
|  | 	assert.True(t, IsRepositoryLimitReached(err)) | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user