code.gitea.io/gitea@v1.22.3/routers/api/v1/repo/transfer.go (about) 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package repo 5 6 import ( 7 "errors" 8 "fmt" 9 "net/http" 10 11 "code.gitea.io/gitea/models" 12 "code.gitea.io/gitea/models/organization" 13 "code.gitea.io/gitea/models/perm" 14 access_model "code.gitea.io/gitea/models/perm/access" 15 repo_model "code.gitea.io/gitea/models/repo" 16 user_model "code.gitea.io/gitea/models/user" 17 "code.gitea.io/gitea/modules/log" 18 api "code.gitea.io/gitea/modules/structs" 19 "code.gitea.io/gitea/modules/web" 20 "code.gitea.io/gitea/services/context" 21 "code.gitea.io/gitea/services/convert" 22 repo_service "code.gitea.io/gitea/services/repository" 23 ) 24 25 // Transfer transfers the ownership of a repository 26 func Transfer(ctx *context.APIContext) { 27 // swagger:operation POST /repos/{owner}/{repo}/transfer repository repoTransfer 28 // --- 29 // summary: Transfer a repo ownership 30 // produces: 31 // - application/json 32 // parameters: 33 // - name: owner 34 // in: path 35 // description: owner of the repo to transfer 36 // type: string 37 // required: true 38 // - name: repo 39 // in: path 40 // description: name of the repo to transfer 41 // type: string 42 // required: true 43 // - name: body 44 // in: body 45 // description: "Transfer Options" 46 // required: true 47 // schema: 48 // "$ref": "#/definitions/TransferRepoOption" 49 // responses: 50 // "202": 51 // "$ref": "#/responses/Repository" 52 // "403": 53 // "$ref": "#/responses/forbidden" 54 // "404": 55 // "$ref": "#/responses/notFound" 56 // "422": 57 // "$ref": "#/responses/validationError" 58 59 opts := web.GetForm(ctx).(*api.TransferRepoOption) 60 61 newOwner, err := user_model.GetUserByName(ctx, opts.NewOwner) 62 if err != nil { 63 if user_model.IsErrUserNotExist(err) { 64 ctx.Error(http.StatusNotFound, "", "The new owner does not exist or cannot be found") 65 return 66 } 67 ctx.InternalServerError(err) 68 return 69 } 70 71 if newOwner.Type == user_model.UserTypeOrganization { 72 if !ctx.Doer.IsAdmin && newOwner.Visibility == api.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) { 73 // The user shouldn't know about this organization 74 ctx.Error(http.StatusNotFound, "", "The new owner does not exist or cannot be found") 75 return 76 } 77 } 78 79 var teams []*organization.Team 80 if opts.TeamIDs != nil { 81 if !newOwner.IsOrganization() { 82 ctx.Error(http.StatusUnprocessableEntity, "repoTransfer", "Teams can only be added to organization-owned repositories") 83 return 84 } 85 86 org := convert.ToOrganization(ctx, organization.OrgFromUser(newOwner)) 87 for _, tID := range *opts.TeamIDs { 88 team, err := organization.GetTeamByID(ctx, tID) 89 if err != nil { 90 ctx.Error(http.StatusUnprocessableEntity, "team", fmt.Errorf("team %d not found", tID)) 91 return 92 } 93 94 if team.OrgID != org.ID { 95 ctx.Error(http.StatusForbidden, "team", fmt.Errorf("team %d belongs not to org %d", tID, org.ID)) 96 return 97 } 98 99 teams = append(teams, team) 100 } 101 } 102 103 if ctx.Repo.GitRepo != nil { 104 ctx.Repo.GitRepo.Close() 105 ctx.Repo.GitRepo = nil 106 } 107 108 oldFullname := ctx.Repo.Repository.FullName() 109 110 if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil { 111 if models.IsErrRepoTransferInProgress(err) { 112 ctx.Error(http.StatusConflict, "StartRepositoryTransfer", err) 113 return 114 } 115 116 if repo_model.IsErrRepoAlreadyExist(err) { 117 ctx.Error(http.StatusUnprocessableEntity, "StartRepositoryTransfer", err) 118 return 119 } 120 121 if errors.Is(err, user_model.ErrBlockedUser) { 122 ctx.Error(http.StatusForbidden, "BlockedUser", err) 123 } else { 124 ctx.InternalServerError(err) 125 } 126 return 127 } 128 129 if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer { 130 log.Trace("Repository transfer initiated: %s -> %s", oldFullname, ctx.Repo.Repository.FullName()) 131 ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeAdmin})) 132 return 133 } 134 135 log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName()) 136 ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeAdmin})) 137 } 138 139 // AcceptTransfer accept a repo transfer 140 func AcceptTransfer(ctx *context.APIContext) { 141 // swagger:operation POST /repos/{owner}/{repo}/transfer/accept repository acceptRepoTransfer 142 // --- 143 // summary: Accept a repo transfer 144 // produces: 145 // - application/json 146 // parameters: 147 // - name: owner 148 // in: path 149 // description: owner of the repo to transfer 150 // type: string 151 // required: true 152 // - name: repo 153 // in: path 154 // description: name of the repo to transfer 155 // type: string 156 // required: true 157 // responses: 158 // "202": 159 // "$ref": "#/responses/Repository" 160 // "403": 161 // "$ref": "#/responses/forbidden" 162 // "404": 163 // "$ref": "#/responses/notFound" 164 165 err := acceptOrRejectRepoTransfer(ctx, true) 166 if ctx.Written() { 167 return 168 } 169 if err != nil { 170 ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err) 171 return 172 } 173 174 ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) 175 } 176 177 // RejectTransfer reject a repo transfer 178 func RejectTransfer(ctx *context.APIContext) { 179 // swagger:operation POST /repos/{owner}/{repo}/transfer/reject repository rejectRepoTransfer 180 // --- 181 // summary: Reject a repo transfer 182 // produces: 183 // - application/json 184 // parameters: 185 // - name: owner 186 // in: path 187 // description: owner of the repo to transfer 188 // type: string 189 // required: true 190 // - name: repo 191 // in: path 192 // description: name of the repo to transfer 193 // type: string 194 // required: true 195 // responses: 196 // "200": 197 // "$ref": "#/responses/Repository" 198 // "403": 199 // "$ref": "#/responses/forbidden" 200 // "404": 201 // "$ref": "#/responses/notFound" 202 203 err := acceptOrRejectRepoTransfer(ctx, false) 204 if ctx.Written() { 205 return 206 } 207 if err != nil { 208 ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err) 209 return 210 } 211 212 ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) 213 } 214 215 func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error { 216 repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) 217 if err != nil { 218 if models.IsErrNoPendingTransfer(err) { 219 ctx.NotFound() 220 return nil 221 } 222 return err 223 } 224 225 if err := repoTransfer.LoadAttributes(ctx); err != nil { 226 return err 227 } 228 229 if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) { 230 ctx.Error(http.StatusForbidden, "CanUserAcceptTransfer", nil) 231 return fmt.Errorf("user does not have permissions to do this") 232 } 233 234 if accept { 235 return repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams) 236 } 237 238 return repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository) 239 }