Refactor repository transfer (#33211)

- Both have `RejectTransfer` and `CancelTransfer` because the permission
checks are not the same. `CancelTransfer` can be done by the doer or
those who have admin permission to access this repository.
`RejectTransfer` can be done by the receiver user if it's an individual
or those who can create repositories if it's an organization.

- Some tests are wrong, this PR corrects them.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
pull/32751/merge
Lunny Xiao 1 day ago committed by GitHub
parent 48183d2b05
commit f88dbf86b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -68,6 +68,7 @@ type RepoTransfer struct { //nolint
RecipientID int64 RecipientID int64
Recipient *user_model.User `xorm:"-"` Recipient *user_model.User `xorm:"-"`
RepoID int64 RepoID int64
Repo *Repository `xorm:"-"`
TeamIDs []int64 TeamIDs []int64
Teams []*organization.Team `xorm:"-"` Teams []*organization.Team `xorm:"-"`
@ -79,48 +80,65 @@ func init() {
db.RegisterModel(new(RepoTransfer)) db.RegisterModel(new(RepoTransfer))
} }
// LoadAttributes fetches the transfer recipient from the database func (r *RepoTransfer) LoadRecipient(ctx context.Context) error {
func (r *RepoTransfer) LoadAttributes(ctx context.Context) error {
if r.Recipient == nil { if r.Recipient == nil {
u, err := user_model.GetUserByID(ctx, r.RecipientID) u, err := user_model.GetUserByID(ctx, r.RecipientID)
if err != nil { if err != nil {
return err return err
} }
r.Recipient = u r.Recipient = u
} }
if r.Recipient.IsOrganization() && len(r.TeamIDs) != len(r.Teams) { return nil
for _, v := range r.TeamIDs { }
team, err := organization.GetTeamByID(ctx, v)
func (r *RepoTransfer) LoadRepo(ctx context.Context) error {
if r.Repo == nil {
repo, err := GetRepositoryByID(ctx, r.RepoID)
if err != nil { if err != nil {
return err return err
} }
r.Repo = repo
}
return nil
}
if team.OrgID != r.Recipient.ID { // LoadAttributes fetches the transfer recipient from the database
return fmt.Errorf("team %d belongs not to org %d", v, r.Recipient.ID) func (r *RepoTransfer) LoadAttributes(ctx context.Context) error {
if err := r.LoadRecipient(ctx); err != nil {
return err
} }
if r.Recipient.IsOrganization() && r.Teams == nil {
teamsMap, err := organization.GetTeamsByIDs(ctx, r.TeamIDs)
if err != nil {
return err
}
for _, team := range teamsMap {
r.Teams = append(r.Teams, team) r.Teams = append(r.Teams, team)
} }
} }
if err := r.LoadRepo(ctx); err != nil {
return err
}
if r.Doer == nil { if r.Doer == nil {
u, err := user_model.GetUserByID(ctx, r.DoerID) u, err := user_model.GetUserByID(ctx, r.DoerID)
if err != nil { if err != nil {
return err return err
} }
r.Doer = u r.Doer = u
} }
return nil return nil
} }
// CanUserAcceptTransfer checks if the user has the rights to accept/decline a repo transfer. // CanUserAcceptOrRejectTransfer checks if the user has the rights to accept/decline a repo transfer.
// For user, it checks if it's himself // For user, it checks if it's himself
// For organizations, it checks if the user is able to create repos // For organizations, it checks if the user is able to create repos
func (r *RepoTransfer) CanUserAcceptTransfer(ctx context.Context, u *user_model.User) bool { func (r *RepoTransfer) CanUserAcceptOrRejectTransfer(ctx context.Context, u *user_model.User) bool {
if err := r.LoadAttributes(ctx); err != nil { if err := r.LoadAttributes(ctx); err != nil {
log.Error("LoadAttributes: %v", err) log.Error("LoadAttributes: %v", err)
return false return false
@ -166,6 +184,10 @@ func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryT
Find(&transfers) Find(&transfers)
} }
func IsRepositoryTransferExist(ctx context.Context, repoID int64) (bool, error) {
return db.GetEngine(ctx).Where("repo_id = ?", repoID).Exist(new(RepoTransfer))
}
// GetPendingRepositoryTransfer fetches the most recent and ongoing transfer // GetPendingRepositoryTransfer fetches the most recent and ongoing transfer
// process for the repository // process for the repository
func GetPendingRepositoryTransfer(ctx context.Context, repo *Repository) (*RepoTransfer, error) { func GetPendingRepositoryTransfer(ctx context.Context, repo *Repository) (*RepoTransfer, error) {
@ -206,11 +228,26 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m
return err return err
} }
if _, err := user_model.GetUserByID(ctx, newOwner.ID); err != nil {
return err
}
// Make sure repo is ready to transfer // Make sure repo is ready to transfer
if err := TestRepositoryReadyForTransfer(repo.Status); err != nil { if err := TestRepositoryReadyForTransfer(repo.Status); err != nil {
return err return err
} }
exist, err := IsRepositoryTransferExist(ctx, repo.ID)
if err != nil {
return err
}
if exist {
return ErrRepoTransferInProgress{
Uname: repo.Owner.LowerName,
Name: repo.Name,
}
}
repo.Status = RepositoryPendingTransfer repo.Status = RepositoryPendingTransfer
if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil { if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil {
return err return err

@ -15,6 +15,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
@ -161,12 +162,16 @@ func AcceptTransfer(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
err := acceptOrRejectRepoTransfer(ctx, true) err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer)
if ctx.Written() {
return
}
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err) switch {
case repo_model.IsErrNoPendingTransfer(err):
ctx.Error(http.StatusNotFound, "AcceptTransferOwnership", err)
case errors.Is(err, util.ErrPermissionDenied):
ctx.Error(http.StatusForbidden, "AcceptTransferOwnership", err)
default:
ctx.Error(http.StatusInternalServerError, "AcceptTransferOwnership", err)
}
return return
} }
@ -199,40 +204,18 @@ func RejectTransfer(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
err := acceptOrRejectRepoTransfer(ctx, false) err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer)
if ctx.Written() {
return
}
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err) switch {
case repo_model.IsErrNoPendingTransfer(err):
ctx.Error(http.StatusNotFound, "RejectRepositoryTransfer", err)
case errors.Is(err, util.ErrPermissionDenied):
ctx.Error(http.StatusForbidden, "RejectRepositoryTransfer", err)
default:
ctx.Error(http.StatusInternalServerError, "RejectRepositoryTransfer", err)
}
return return
} }
ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission))
} }
func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error {
repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
if err != nil {
if repo_model.IsErrNoPendingTransfer(err) {
ctx.NotFound()
return nil
}
return err
}
if err := repoTransfer.LoadAttributes(ctx); err != nil {
return err
}
if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) {
ctx.Error(http.StatusForbidden, "CanUserAcceptTransfer", nil)
return fmt.Errorf("user does not have permissions to do this")
}
if accept {
return repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams)
}
return repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository)
}

@ -309,6 +309,36 @@ const (
tplStarUnstar templates.TplName = "repo/star_unstar" tplStarUnstar templates.TplName = "repo/star_unstar"
) )
func acceptTransfer(ctx *context.Context) {
err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer)
if err == nil {
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
ctx.Redirect(ctx.Repo.Repository.Link())
return
}
handleActionError(ctx, err)
}
func rejectTransfer(ctx *context.Context) {
err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer)
if err == nil {
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
ctx.Redirect(ctx.Repo.Repository.Link())
return
}
handleActionError(ctx, err)
}
func handleActionError(ctx *context.Context, err error) {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
} else if errors.Is(err, util.ErrPermissionDenied) {
ctx.Error(http.StatusNotFound)
} else {
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err)
}
}
// Action response for actions to a repository // Action response for actions to a repository
func Action(ctx *context.Context) { func Action(ctx *context.Context) {
var err error var err error
@ -322,9 +352,11 @@ func Action(ctx *context.Context) {
case "unstar": case "unstar":
err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
case "accept_transfer": case "accept_transfer":
err = acceptOrRejectRepoTransfer(ctx, true) acceptTransfer(ctx)
return
case "reject_transfer": case "reject_transfer":
err = acceptOrRejectRepoTransfer(ctx, false) rejectTransfer(ctx)
return
case "desc": // FIXME: this is not used case "desc": // FIXME: this is not used
if !ctx.Repo.IsOwner() { if !ctx.Repo.IsOwner() {
ctx.Error(http.StatusNotFound) ctx.Error(http.StatusNotFound)
@ -337,13 +369,9 @@ func Action(ctx *context.Context) {
} }
if err != nil { if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) { handleActionError(ctx, err)
ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
} else {
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err)
return return
} }
}
switch ctx.PathParam("action") { switch ctx.PathParam("action") {
case "watch", "unwatch": case "watch", "unwatch":
@ -377,41 +405,6 @@ func Action(ctx *context.Context) {
ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"), ctx.Repo.RepoLink) ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
} }
func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
if err != nil {
return err
}
if err := repoTransfer.LoadAttributes(ctx); err != nil {
return err
}
if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) {
return errors.New("user does not have enough permissions")
}
if accept {
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
}
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
} else {
if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil {
return err
}
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
}
ctx.Redirect(ctx.Repo.Repository.Link())
return nil
}
// RedirectDownload return a file based on the following infos: // RedirectDownload return a file based on the following infos:
func RedirectDownload(ctx *context.Context) { func RedirectDownload(ctx *context.Context) {
var ( var (

@ -832,16 +832,10 @@ func SettingsPost(ctx *context.Context) {
} else { } else {
ctx.ServerError("GetPendingRepositoryTransfer", err) ctx.ServerError("GetPendingRepositoryTransfer", err)
} }
return
}
if err := repoTransfer.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadRecipient", err)
return return
} }
if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil { if err := repo_service.CancelRepositoryTransfer(ctx, repoTransfer, ctx.Doer); err != nil {
ctx.ServerError("CancelRepositoryTransfer", err) ctx.ServerError("CancelRepositoryTransfer", err)
return return
} }

@ -653,7 +653,7 @@ func RepoAssignment(ctx *Context) {
ctx.Data["RepoTransfer"] = repoTransfer ctx.Data["RepoTransfer"] = repoTransfer
if ctx.Doer != nil { if ctx.Doer != nil {
ctx.Data["CanUserAcceptTransfer"] = repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) ctx.Data["CanUserAcceptOrRejectTransfer"] = repoTransfer.CanUserAcceptOrRejectTransfer(ctx, ctx.Doer)
} }
} }

@ -272,9 +272,9 @@ func MigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo
} }
// TransferRepository notifies create repository to notifiers // TransferRepository notifies create repository to notifiers
func TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, newOwnerName string) { func TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
for _, notifier := range notifiers { for _, notifier := range notifiers {
notifier.TransferRepository(ctx, doer, repo, newOwnerName) notifier.TransferRepository(ctx, doer, repo, oldOwnerName)
} }
} }

@ -27,19 +27,8 @@ func getRepoWorkingLockKey(repoID int64) string {
return fmt.Sprintf("repo_working_%d", repoID) return fmt.Sprintf("repo_working_%d", repoID)
} }
// TransferOwnership transfers all corresponding setting from old user to new one. // AcceptTransferOwnership transfers all corresponding setting from old user to new one.
func TransferOwnership(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error { func AcceptTransferOwnership(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) error {
if err := repo.LoadOwner(ctx); err != nil {
return err
}
for _, team := range teams {
if newOwner.ID != team.OrgID {
return fmt.Errorf("team %d does not belong to organization", team.ID)
}
}
oldOwner := repo.Owner
releaser, err := globallock.Lock(ctx, getRepoWorkingLockKey(repo.ID)) releaser, err := globallock.Lock(ctx, getRepoWorkingLockKey(repo.ID))
if err != nil { if err != nil {
log.Error("lock.Lock(): %v", err) log.Error("lock.Lock(): %v", err)
@ -47,29 +36,44 @@ func TransferOwnership(ctx context.Context, doer, newOwner *user_model.User, rep
} }
defer releaser() defer releaser()
if err := transferOwnership(ctx, doer, newOwner.Name, repo); err != nil { repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, repo)
if err != nil {
return err return err
} }
releaser()
newRepo, err := repo_model.GetRepositoryByID(ctx, repo.ID) oldOwnerName := repo.OwnerName
if err != nil {
if err := db.WithTx(ctx, func(ctx context.Context) error {
if err := repoTransfer.LoadAttributes(ctx); err != nil {
return err return err
} }
for _, team := range teams { if !repoTransfer.CanUserAcceptOrRejectTransfer(ctx, doer) {
if err := addRepositoryToTeam(ctx, team, newRepo); err != nil { return util.ErrPermissionDenied
}
if err := repo.LoadOwner(ctx); err != nil {
return err return err
} }
for _, team := range repoTransfer.Teams {
if repoTransfer.Recipient.ID != team.OrgID {
return fmt.Errorf("team %d does not belong to organization", team.ID)
}
} }
notify_service.TransferRepository(ctx, doer, repo, oldOwner.Name) return transferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient.Name, repo, repoTransfer.Teams)
}); err != nil {
return err
}
releaser()
notify_service.TransferRepository(ctx, doer, repo, oldOwnerName)
return nil return nil
} }
// transferOwnership transfers all corresponding repository items from old user to new one. // transferOwnership transfers all corresponding repository items from old user to new one.
func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName string, repo *repo_model.Repository) (err error) { func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName string, repo *repo_model.Repository, teams []*organization.Team) (err error) {
repoRenamed := false repoRenamed := false
wikiRenamed := false wikiRenamed := false
oldOwnerName := doer.Name oldOwnerName := doer.Name
@ -138,7 +142,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
repo.OwnerName = newOwner.Name repo.OwnerName = newOwner.Name
// Update repository. // Update repository.
if _, err := sess.ID(repo.ID).Update(repo); err != nil { if err := repo_model.UpdateRepositoryCols(ctx, repo, "owner_id", "owner_name"); err != nil {
return fmt.Errorf("update owner: %w", err) return fmt.Errorf("update owner: %w", err)
} }
@ -174,15 +178,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
collaboration.UserID = 0 collaboration.UserID = 0
} }
// Remove old team-repository relations.
if oldOwner.IsOrganization() { if oldOwner.IsOrganization() {
// Remove old team-repository relations.
if err := organization.RemoveOrgRepo(ctx, oldOwner.ID, repo.ID); err != nil { if err := organization.RemoveOrgRepo(ctx, oldOwner.ID, repo.ID); err != nil {
return fmt.Errorf("removeOrgRepo: %w", err) return fmt.Errorf("removeOrgRepo: %w", err)
} }
}
// Remove project's issues that belong to old organization's projects // Remove project's issues that belong to old organization's projects
if oldOwner.IsOrganization() {
projects, err := project_model.GetAllProjectsIDsByOwnerIDAndType(ctx, oldOwner.ID, project_model.TypeOrganization) projects, err := project_model.GetAllProjectsIDsByOwnerIDAndType(ctx, oldOwner.ID, project_model.TypeOrganization)
if err != nil { if err != nil {
return fmt.Errorf("Unable to find old org projects: %w", err) return fmt.Errorf("Unable to find old org projects: %w", err)
@ -225,15 +227,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
return fmt.Errorf("watchRepo: %w", err) return fmt.Errorf("watchRepo: %w", err)
} }
// Remove watch for organization.
if oldOwner.IsOrganization() { if oldOwner.IsOrganization() {
// Remove watch for organization.
if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil { if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil {
return fmt.Errorf("watchRepo [false]: %w", err) return fmt.Errorf("watchRepo [false]: %w", err)
} }
}
// Delete labels that belong to the old organization and comments that added these labels // Delete labels that belong to the old organization and comments that added these labels
if oldOwner.IsOrganization() {
if _, err := sess.Exec(`DELETE FROM issue_label WHERE issue_label.id IN ( if _, err := sess.Exec(`DELETE FROM issue_label WHERE issue_label.id IN (
SELECT il_too.id FROM ( SELECT il_too.id FROM (
SELECT il_too_too.id SELECT il_too_too.id
@ -261,7 +261,6 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
// Rename remote repository to new path and delete local copy. // Rename remote repository to new path and delete local copy.
dir := user_model.UserPath(newOwner.Name) dir := user_model.UserPath(newOwner.Name)
if err := os.MkdirAll(dir, os.ModePerm); err != nil { if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("Failed to create dir %s: %w", dir, err) return fmt.Errorf("Failed to create dir %s: %w", dir, err)
} }
@ -273,7 +272,6 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
// Rename remote wiki repository to new path and delete local copy. // Rename remote wiki repository to new path and delete local copy.
wikiPath := repo_model.WikiPath(oldOwner.Name, repo.Name) wikiPath := repo_model.WikiPath(oldOwner.Name, repo.Name)
if isExist, err := util.IsExist(wikiPath); err != nil { if isExist, err := util.IsExist(wikiPath); err != nil {
log.Error("Unable to check if %s exists. Error: %v", wikiPath, err) log.Error("Unable to check if %s exists. Error: %v", wikiPath, err)
return err return err
@ -301,6 +299,17 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
return fmt.Errorf("repo_model.NewRedirect: %w", err) return fmt.Errorf("repo_model.NewRedirect: %w", err)
} }
newRepo, err := repo_model.GetRepositoryByID(ctx, repo.ID)
if err != nil {
return err
}
for _, team := range teams {
if err := addRepositoryToTeam(ctx, team, newRepo); err != nil {
return err
}
}
return committer.Commit() return committer.Commit()
} }
@ -343,17 +352,9 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR
} }
} }
ctx, committer, err := db.TxContext(ctx) return db.WithTx(ctx, func(ctx context.Context) error {
if err != nil { return repo_model.NewRedirect(ctx, repo.Owner.ID, repo.ID, oldRepoName, newRepoName)
return err })
}
defer committer.Close()
if err := repo_model.NewRedirect(ctx, repo.Owner.ID, repo.ID, oldRepoName, newRepoName); err != nil {
return err
}
return committer.Commit()
} }
// ChangeRepositoryName changes all corresponding setting from old repository name to new one. // ChangeRepositoryName changes all corresponding setting from old repository name to new one.
@ -387,13 +388,25 @@ func ChangeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo
// StartRepositoryTransfer transfer a repo from one owner to a new one. // StartRepositoryTransfer transfer a repo from one owner to a new one.
// it make repository into pending transfer state, if doer can not create repo for new owner. // it make repository into pending transfer state, if doer can not create repo for new owner.
func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error { func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error {
releaser, err := globallock.Lock(ctx, getRepoWorkingLockKey(repo.ID))
if err != nil {
return fmt.Errorf("lock.Lock: %w", err)
}
defer releaser()
if err := repo_model.TestRepositoryReadyForTransfer(repo.Status); err != nil { if err := repo_model.TestRepositoryReadyForTransfer(repo.Status); err != nil {
return err return err
} }
// Admin is always allowed to transfer || user transfer repo back to his account var isDirectTransfer bool
oldOwnerName := repo.OwnerName
if err := db.WithTx(ctx, func(ctx context.Context) error {
// Admin is always allowed to transfer || user transfer repo back to his account,
// then it will transfer directly without acceptance.
if doer.IsAdmin || doer.ID == newOwner.ID { if doer.IsAdmin || doer.ID == newOwner.ID {
return TransferOwnership(ctx, doer, newOwner, repo, teams) isDirectTransfer = true
return transferOwnership(ctx, doer, newOwner.Name, repo, teams)
} }
if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) { if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) {
@ -407,7 +420,8 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
return err return err
} }
if allowed { if allowed {
return TransferOwnership(ctx, doer, newOwner, repo, teams) isDirectTransfer = true
return transferOwnership(ctx, doer, newOwner.Name, repo, teams)
} }
} }
@ -424,33 +438,92 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
// Make repo as pending for transfer // Make repo as pending for transfer
repo.Status = repo_model.RepositoryPendingTransfer repo.Status = repo_model.RepositoryPendingTransfer
if err := repo_model.CreatePendingRepositoryTransfer(ctx, doer, newOwner, repo.ID, teams); err != nil { return repo_model.CreatePendingRepositoryTransfer(ctx, doer, newOwner, repo.ID, teams)
}); err != nil {
return err return err
} }
if isDirectTransfer {
notify_service.TransferRepository(ctx, doer, repo, oldOwnerName)
} else {
// notify users who are able to accept / reject transfer // notify users who are able to accept / reject transfer
notify_service.RepoPendingTransfer(ctx, doer, newOwner, repo) notify_service.RepoPendingTransfer(ctx, doer, newOwner, repo)
}
return nil return nil
} }
// CancelRepositoryTransfer marks the repository as ready and remove pending transfer entry, // RejectRepositoryTransfer marks the repository as ready and remove pending transfer entry,
// thus cancel the transfer process. // thus cancel the transfer process.
func CancelRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) error { // The accepter can reject the transfer.
ctx, committer, err := db.TxContext(ctx) func RejectRepositoryTransfer(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) error {
return db.WithTx(ctx, func(ctx context.Context) error {
repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, repo)
if err != nil { if err != nil {
return err return err
} }
defer committer.Close()
if err := repoTransfer.LoadAttributes(ctx); err != nil {
return err
}
if !repoTransfer.CanUserAcceptOrRejectTransfer(ctx, doer) {
return util.ErrPermissionDenied
}
repo.Status = repo_model.RepositoryReady repo.Status = repo_model.RepositoryReady
if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil { if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil {
return err return err
} }
if err := repo_model.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { return repo_model.DeleteRepositoryTransfer(ctx, repo.ID)
})
}
func canUserCancelTransfer(ctx context.Context, r *repo_model.RepoTransfer, u *user_model.User) bool {
if u.IsAdmin || u.ID == r.DoerID {
return true
}
if err := r.LoadAttributes(ctx); err != nil {
log.Error("LoadAttributes: %v", err)
return false
}
if err := r.Repo.LoadOwner(ctx); err != nil {
log.Error("LoadOwner: %v", err)
return false
}
if !r.Repo.Owner.IsOrganization() {
return r.Repo.OwnerID == u.ID
}
perm, err := access_model.GetUserRepoPermission(ctx, r.Repo, u)
if err != nil {
log.Error("GetUserRepoPermission: %v", err)
return false
}
return perm.IsOwner()
}
// CancelRepositoryTransfer cancels the repository transfer process. The sender or
// the users who have admin permission of the original repository can cancel the transfer
func CancelRepositoryTransfer(ctx context.Context, repoTransfer *repo_model.RepoTransfer, doer *user_model.User) error {
return db.WithTx(ctx, func(ctx context.Context) error {
if err := repoTransfer.LoadAttributes(ctx); err != nil {
return err return err
} }
return committer.Commit() if !canUserCancelTransfer(ctx, repoTransfer, doer) {
return util.ErrPermissionDenied
}
repoTransfer.Repo.Status = repo_model.RepositoryReady
if err := repo_model.UpdateRepositoryCols(ctx, repoTransfer.Repo, "status"); err != nil {
return err
}
return repo_model.DeleteRepositoryTransfer(ctx, repoTransfer.RepoID)
})
} }

@ -34,23 +34,26 @@ func TestTransferOwnership(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
repo.Owner = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) assert.NoError(t, repo.LoadOwner(db.DefaultContext))
assert.NoError(t, TransferOwnership(db.DefaultContext, doer, doer, repo, nil)) repoTransfer := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoTransfer{ID: 1})
assert.NoError(t, repoTransfer.LoadAttributes(db.DefaultContext))
assert.NoError(t, AcceptTransferOwnership(db.DefaultContext, repo, doer))
transferredRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) transferredRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
assert.EqualValues(t, 2, transferredRepo.OwnerID) assert.EqualValues(t, 1, transferredRepo.OwnerID) // repo_transfer.yml id=1
unittest.AssertNotExistsBean(t, &repo_model.RepoTransfer{ID: 1})
exist, err := util.IsExist(repo_model.RepoPath("org3", "repo3")) exist, err := util.IsExist(repo_model.RepoPath("org3", "repo3"))
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, exist) assert.False(t, exist)
exist, err = util.IsExist(repo_model.RepoPath("user2", "repo3")) exist, err = util.IsExist(repo_model.RepoPath("user1", "repo3"))
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, exist) assert.True(t, exist)
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
OpType: activities_model.ActionTransferRepo, OpType: activities_model.ActionTransferRepo,
ActUserID: 2, ActUserID: 1,
RepoID: 3, RepoID: 3,
Content: "org3/repo3", Content: "org3/repo3",
}) })
@ -61,10 +64,10 @@ func TestTransferOwnership(t *testing.T) {
func TestStartRepositoryTransferSetPermission(t *testing.T) { func TestStartRepositoryTransferSetPermission(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
repo.Owner = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) assert.NoError(t, repo.LoadOwner(db.DefaultContext))
hasAccess, err := access_model.HasAnyUnitAccess(db.DefaultContext, recipient.ID, repo) hasAccess, err := access_model.HasAnyUnitAccess(db.DefaultContext, recipient.ID, repo)
assert.NoError(t, err) assert.NoError(t, err)
@ -82,7 +85,7 @@ func TestStartRepositoryTransferSetPermission(t *testing.T) {
func TestRepositoryTransfer(t *testing.T) { func TestRepositoryTransfer(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
transfer, err := repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) transfer, err := repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo)
@ -90,7 +93,7 @@ func TestRepositoryTransfer(t *testing.T) {
assert.NotNil(t, transfer) assert.NotNil(t, transfer)
// Cancel transfer // Cancel transfer
assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo)) assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, transfer, doer))
transfer, err = repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) transfer, err = repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo)
assert.Error(t, err) assert.Error(t, err)
@ -113,10 +116,12 @@ func TestRepositoryTransfer(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
assert.True(t, repo_model.IsErrRepoTransferInProgress(err)) assert.True(t, repo_model.IsErrRepoTransferInProgress(err))
// Unknown user repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
err = repo_model.CreatePendingRepositoryTransfer(db.DefaultContext, doer, &user_model.User{ID: 1000, LowerName: "user1000"}, repo.ID, nil) // Unknown user, transfer non-existent transfer repo id = 2
err = repo_model.CreatePendingRepositoryTransfer(db.DefaultContext, doer, &user_model.User{ID: 1000, LowerName: "user1000"}, repo2.ID, nil)
assert.Error(t, err) assert.Error(t, err)
// Cancel transfer // Reject transfer
assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo)) err = RejectRepositoryTransfer(db.DefaultContext, repo2, doer)
assert.True(t, repo_model.IsErrNoPendingTransfer(err))
} }

@ -117,10 +117,10 @@ func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, not
} }
// cancel each other repository transfers // cancel each other repository transfers
if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil { if err := cancelRepositoryTransfers(ctx, doer, blocker, blockee); err != nil {
return err return err
} }
if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil { if err := cancelRepositoryTransfers(ctx, doer, blockee, blocker); err != nil {
return err return err
} }
@ -192,7 +192,7 @@ func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) erro
} }
} }
func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error { func cancelRepositoryTransfers(ctx context.Context, doer, sender, recipient *user_model.User) error {
transfers, err := repo_model.GetPendingRepositoryTransfers(ctx, &repo_model.PendingRepositoryTransferOptions{ transfers, err := repo_model.GetPendingRepositoryTransfers(ctx, &repo_model.PendingRepositoryTransferOptions{
SenderID: sender.ID, SenderID: sender.ID,
RecipientID: recipient.ID, RecipientID: recipient.ID,
@ -202,12 +202,7 @@ func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_mode
} }
for _, transfer := range transfers { for _, transfer := range transfers {
repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID) if err := repo_service.CancelRepositoryTransfer(ctx, transfer, doer); err != nil {
if err != nil {
return err
}
if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil {
return err return err
} }
} }

@ -39,16 +39,16 @@
{{if $.RepoTransfer}} {{if $.RepoTransfer}}
<form method="post" action="{{$.RepoLink}}/action/accept_transfer?redirect_to={{$.RepoLink}}"> <form method="post" action="{{$.RepoLink}}/action/accept_transfer?redirect_to={{$.RepoLink}}">
{{$.CsrfTokenHtml}} {{$.CsrfTokenHtml}}
<div data-tooltip-content="{{if $.CanUserAcceptTransfer}}{{ctx.Locale.Tr "repo.transfer.accept_desc" $.RepoTransfer.Recipient.DisplayName}}{{else}}{{ctx.Locale.Tr "repo.transfer.no_permission_to_accept"}}{{end}}"> <div data-tooltip-content="{{if $.CanUserAcceptOrRejectTransfer}}{{ctx.Locale.Tr "repo.transfer.accept_desc" $.RepoTransfer.Recipient.DisplayName}}{{else}}{{ctx.Locale.Tr "repo.transfer.no_permission_to_accept"}}{{end}}">
<button type="submit" class="ui basic button {{if $.CanUserAcceptTransfer}}primary {{end}} ok small"{{if not $.CanUserAcceptTransfer}} disabled{{end}}> <button type="submit" class="ui basic button {{if $.CanUserAcceptOrRejectTransfer}}primary {{end}} ok small"{{if not $.CanUserAcceptOrRejectTransfer}} disabled{{end}}>
{{ctx.Locale.Tr "repo.transfer.accept"}} {{ctx.Locale.Tr "repo.transfer.accept"}}
</button> </button>
</div> </div>
</form> </form>
<form method="post" action="{{$.RepoLink}}/action/reject_transfer?redirect_to={{$.RepoLink}}"> <form method="post" action="{{$.RepoLink}}/action/reject_transfer?redirect_to={{$.RepoLink}}">
{{$.CsrfTokenHtml}} {{$.CsrfTokenHtml}}
<div data-tooltip-content="{{if $.CanUserAcceptTransfer}}{{ctx.Locale.Tr "repo.transfer.reject_desc" $.RepoTransfer.Recipient.DisplayName}}{{else}}{{ctx.Locale.Tr "repo.transfer.no_permission_to_reject"}}{{end}}"> <div data-tooltip-content="{{if $.CanUserAcceptOrRejectTransfer}}{{ctx.Locale.Tr "repo.transfer.reject_desc" $.RepoTransfer.Recipient.DisplayName}}{{else}}{{ctx.Locale.Tr "repo.transfer.no_permission_to_reject"}}{{end}}">
<button type="submit" class="ui basic button {{if $.CanUserAcceptTransfer}}red {{end}}ok small"{{if not $.CanUserAcceptTransfer}} disabled{{end}}> <button type="submit" class="ui basic button {{if $.CanUserAcceptOrRejectTransfer}}red {{end}}ok small"{{if not $.CanUserAcceptOrRejectTransfer}} disabled{{end}}>
{{ctx.Locale.Tr "repo.transfer.reject"}} {{ctx.Locale.Tr "repo.transfer.reject"}}
</button> </button>
</div> </div>

Loading…
Cancel
Save