Skip to content

Commit bffa303

Browse files
authored
Add option to purge users (#18064)
Add the ability to purge users when deleting them. Close #15588 Signed-off-by: Andrew Thornton <[email protected]>
1 parent 1757053 commit bffa303

File tree

16 files changed

+221
-51
lines changed

16 files changed

+221
-51
lines changed

cmd/admin.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ var (
157157
Name: "email,e",
158158
Usage: "Email of the user to delete",
159159
},
160+
cli.BoolFlag{
161+
Name: "purge",
162+
Usage: "Purge user, all their repositories, organizations and comments",
163+
},
160164
},
161165
Action: runDeleteUser,
162166
}
@@ -675,7 +679,7 @@ func runDeleteUser(c *cli.Context) error {
675679
return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id"))
676680
}
677681

678-
return user_service.DeleteUser(user)
682+
return user_service.DeleteUser(ctx, user, c.Bool("purge"))
679683
}
680684

681685
func runGenerateAccessToken(c *cli.Context) error {

integrations/admin_user_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func TestAdminDeleteUser(t *testing.T) {
7676
req := NewRequestWithValues(t, "POST", "/admin/users/8/delete", map[string]string{
7777
"_csrf": csrf,
7878
})
79-
session.MakeRequest(t, req, http.StatusOK)
79+
session.MakeRequest(t, req, http.StatusSeeOther)
8080

8181
assertUserDeleted(t, 8)
8282
unittest.CheckConsistencyFor(t, &user_model.User{})

integrations/integration_test.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,13 @@ func initIntegrationTest() {
188188

189189
switch {
190190
case setting.Database.UseMySQL:
191-
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/",
192-
setting.Database.User, setting.Database.Passwd, setting.Database.Host))
191+
connType := "tcp"
192+
if len(setting.Database.Host) > 0 && setting.Database.Host[0] == '/' { // looks like a unix socket
193+
connType = "unix"
194+
}
195+
196+
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@%s(%s)/",
197+
setting.Database.User, setting.Database.Passwd, connType, setting.Database.Host))
193198
defer db.Close()
194199
if err != nil {
195200
log.Fatal("sql.Open: %v", err)

models/packages/package_version.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType
107107
ExactMatch: true,
108108
Value: version,
109109
},
110-
IsInternal: isInternal,
110+
IsInternal: util.OptionalBoolOf(isInternal),
111111
Paginator: db.NewAbsoluteListOptions(0, 1),
112112
})
113113
if err != nil {
@@ -171,15 +171,18 @@ type PackageSearchOptions struct {
171171
Name SearchValue // only results with the specific name are found
172172
Version SearchValue // only results with the specific version are found
173173
Properties map[string]string // only results are found which contain all listed version properties with the specific value
174-
IsInternal bool
174+
IsInternal util.OptionalBool
175175
HasFileWithName string // only results are found which are associated with a file with the specific name
176176
HasFiles util.OptionalBool // only results are found which have associated files
177177
Sort string
178178
db.Paginator
179179
}
180180

181181
func (opts *PackageSearchOptions) toConds() builder.Cond {
182-
var cond builder.Cond = builder.Eq{"package_version.is_internal": opts.IsInternal}
182+
cond := builder.NewCond()
183+
if !opts.IsInternal.IsNone() {
184+
cond = builder.Eq{"package_version.is_internal": opts.IsInternal.IsTrue()}
185+
}
183186

184187
if opts.OwnerID != 0 {
185188
cond = cond.And(builder.Eq{"package.owner_id": opts.OwnerID})

models/project/project.go

+37
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,40 @@ func DeleteProjectByIDCtx(ctx context.Context, id int64) error {
330330

331331
return updateRepositoryProjectCount(ctx, p.RepoID)
332332
}
333+
334+
func DeleteProjectByRepoIDCtx(ctx context.Context, repoID int64) error {
335+
switch {
336+
case setting.Database.UseSQLite3:
337+
if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_issue WHERE project_issue.id IN (SELECT project_issue.id FROM project_issue INNER JOIN project WHERE project.id = project_issue.project_id AND project.repo_id = ?)", repoID); err != nil {
338+
return err
339+
}
340+
if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_board WHERE project_board.id IN (SELECT project_board.id FROM project_board INNER JOIN project WHERE project.id = project_board.project_id AND project.repo_id = ?)", repoID); err != nil {
341+
return err
342+
}
343+
if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
344+
return err
345+
}
346+
case setting.Database.UsePostgreSQL:
347+
if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_issue USING project WHERE project.id = project_issue.project_id AND project.repo_id = ? ", repoID); err != nil {
348+
return err
349+
}
350+
if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_board USING project WHERE project.id = project_board.project_id AND project.repo_id = ? ", repoID); err != nil {
351+
return err
352+
}
353+
if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
354+
return err
355+
}
356+
default:
357+
if _, err := db.GetEngine(ctx).Exec("DELETE project_issue FROM project_issue INNER JOIN project ON project.id = project_issue.project_id WHERE project.repo_id = ? ", repoID); err != nil {
358+
return err
359+
}
360+
if _, err := db.GetEngine(ctx).Exec("DELETE project_board FROM project_board INNER JOIN project ON project.id = project_board.project_id WHERE project.repo_id = ? ", repoID); err != nil {
361+
return err
362+
}
363+
if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
364+
return err
365+
}
366+
}
367+
368+
return updateRepositoryProjectCount(ctx, repoID)
369+
}

models/repo.go

+2-10
Original file line numberDiff line numberDiff line change
@@ -342,16 +342,8 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
342342
}
343343
}
344344

345-
projects, _, err := project_model.GetProjects(ctx, project_model.SearchOptions{
346-
RepoID: repoID,
347-
})
348-
if err != nil {
349-
return fmt.Errorf("get projects: %v", err)
350-
}
351-
for i := range projects {
352-
if err := project_model.DeleteProjectByIDCtx(ctx, projects[i].ID); err != nil {
353-
return fmt.Errorf("delete project [%d]: %v", projects[i].ID, err)
354-
}
345+
if err := project_model.DeleteProjectByRepoIDCtx(ctx, repoID); err != nil {
346+
return fmt.Errorf("unable to delete projects for repo[%d]: %v", repoID, err)
355347
}
356348

357349
// Remove LFS objects

models/user.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727
)
2828

2929
// DeleteUser deletes models associated to an user.
30-
func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
30+
func DeleteUser(ctx context.Context, u *user_model.User, purge bool) (err error) {
3131
e := db.GetEngine(ctx)
3232

3333
// ***** START: Watch *****
@@ -95,8 +95,8 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
9595
return err
9696
}
9797

98-
if setting.Service.UserDeleteWithCommentsMaxTime != 0 &&
99-
u.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now()) {
98+
if purge || (setting.Service.UserDeleteWithCommentsMaxTime != 0 &&
99+
u.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now())) {
100100

101101
// Delete Comments
102102
const batchSize = 50

options/locale/locale_en-US.ini

+2
Original file line numberDiff line numberDiff line change
@@ -2540,6 +2540,8 @@ users.delete_account = Delete User Account
25402540
users.cannot_delete_self = "You cannot delete yourself"
25412541
users.still_own_repo = This user still owns one or more repositories. Delete or transfer these repositories first.
25422542
users.still_has_org = This user is a member of an organization. Remove the user from any organizations first.
2543+
users.purge = Purge User
2544+
users.purge_help = Forcibly delete user and any repositories, organizations, and packages owned by the user. All comments will be deleted too.
25432545
users.still_own_packages = This user still owns one or more packages. Delete these packages first.
25442546
users.deletion_success = The user account has been deleted.
25452547
users.reset_2fa = Reset 2FA

routers/api/v1/admin/user.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ func DeleteUser(ctx *context.APIContext) {
316316
return
317317
}
318318

319-
if err := user_service.DeleteUser(ctx.ContextUser); err != nil {
319+
if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
320320
if models.IsErrUserOwnRepos(err) ||
321321
models.IsErrUserHasOrgs(err) ||
322322
models.IsErrUserOwnPackages(err) {

routers/web/admin/users.go

+6-16
Original file line numberDiff line numberDiff line change
@@ -419,29 +419,21 @@ func DeleteUser(ctx *context.Context) {
419419
// admin should not delete themself
420420
if u.ID == ctx.Doer.ID {
421421
ctx.Flash.Error(ctx.Tr("admin.users.cannot_delete_self"))
422-
ctx.JSON(http.StatusOK, map[string]interface{}{
423-
"redirect": setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")),
424-
})
422+
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
425423
return
426424
}
427425

428-
if err = user_service.DeleteUser(u); err != nil {
426+
if err = user_service.DeleteUser(ctx, u, ctx.FormBool("purge")); err != nil {
429427
switch {
430428
case models.IsErrUserOwnRepos(err):
431429
ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo"))
432-
ctx.JSON(http.StatusOK, map[string]interface{}{
433-
"redirect": setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")),
434-
})
430+
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
435431
case models.IsErrUserHasOrgs(err):
436432
ctx.Flash.Error(ctx.Tr("admin.users.still_has_org"))
437-
ctx.JSON(http.StatusOK, map[string]interface{}{
438-
"redirect": setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")),
439-
})
433+
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
440434
case models.IsErrUserOwnPackages(err):
441435
ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages"))
442-
ctx.JSON(http.StatusOK, map[string]interface{}{
443-
"redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"),
444-
})
436+
ctx.Redirect(setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"))
445437
default:
446438
ctx.ServerError("DeleteUser", err)
447439
}
@@ -450,9 +442,7 @@ func DeleteUser(ctx *context.Context) {
450442
log.Trace("Account deleted by admin (%s): %s", ctx.Doer.Name, u.Name)
451443

452444
ctx.Flash.Success(ctx.Tr("admin.users.deletion_success"))
453-
ctx.JSON(http.StatusOK, map[string]interface{}{
454-
"redirect": setting.AppSubURL + "/admin/users",
455-
})
445+
ctx.Redirect(setting.AppSubURL + "/admin/users")
456446
}
457447

458448
// AvatarPost response for change user's avatar request

routers/web/user/setting/account.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ func DeleteAccount(ctx *context.Context) {
248248
return
249249
}
250250

251-
if err := user.DeleteUser(ctx.Doer); err != nil {
251+
if err := user.DeleteUser(ctx, ctx.Doer, false); err != nil {
252252
switch {
253253
case models.IsErrUserOwnRepos(err):
254254
ctx.Flash.Error(ctx.Tr("form.still_own_repo"))

services/packages/container/cleanup.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e
5959
ExactMatch: true,
6060
Value: container_model.UploadVersion,
6161
},
62-
IsInternal: true,
62+
IsInternal: util.OptionalBoolTrue,
6363
HasFiles: util.OptionalBoolFalse,
6464
})
6565
if err != nil {

services/packages/packages.go

+28
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"code.gitea.io/gitea/models/db"
1515
packages_model "code.gitea.io/gitea/models/packages"
16+
repo_model "code.gitea.io/gitea/models/repo"
1617
user_model "code.gitea.io/gitea/models/user"
1718
"code.gitea.io/gitea/modules/json"
1819
"code.gitea.io/gitea/modules/log"
@@ -451,3 +452,30 @@ func GetPackageFileStream(ctx context.Context, pf *packages_model.PackageFile) (
451452
}
452453
return s, pf, err
453454
}
455+
456+
// RemoveAllPackages for User
457+
func RemoveAllPackages(ctx context.Context, userID int64) (int, error) {
458+
count := 0
459+
for {
460+
pkgVersions, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
461+
Paginator: &db.ListOptions{
462+
PageSize: repo_model.RepositoryListDefaultPageSize,
463+
Page: 1,
464+
},
465+
OwnerID: userID,
466+
})
467+
if err != nil {
468+
return count, fmt.Errorf("GetOwnedPackages[%d]: %w", userID, err)
469+
}
470+
if len(pkgVersions) == 0 {
471+
break
472+
}
473+
for _, pv := range pkgVersions {
474+
if err := DeletePackageVersionAndReferences(ctx, pv); err != nil {
475+
return count, fmt.Errorf("unable to delete package %d:%s[%d]. Error: %w", pv.PackageID, pv.Version, pv.ID, err)
476+
}
477+
count++
478+
}
479+
}
480+
return count, nil
481+
}

0 commit comments

Comments
 (0)