diff options
author | zeripath <art27@cantab.net> | 2022-07-14 08:22:09 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-14 08:22:09 +0100 |
commit | bffa30302070b594a1c40cdc56264b9731036fb3 (patch) | |
tree | 92104ff6b8a51f5d1506742427dd1399fa42428c /services | |
parent | 175705356cac06c22d13d86b31605a6ad6dd9642 (diff) | |
download | gitea-bffa30302070b594a1c40cdc56264b9731036fb3.tar.gz gitea-bffa30302070b594a1c40cdc56264b9731036fb3.zip |
Add option to purge users (#18064)
Add the ability to purge users when deleting them.
Close #15588
Signed-off-by: Andrew Thornton <art27@cantab.net>
Diffstat (limited to 'services')
-rw-r--r-- | services/packages/container/cleanup.go | 2 | ||||
-rw-r--r-- | services/packages/packages.go | 28 | ||||
-rw-r--r-- | services/user/user.go | 106 | ||||
-rw-r--r-- | services/user/user_test.go | 10 |
4 files changed, 136 insertions, 10 deletions
diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go index 390a0b7b05..3e44f9aa1a 100644 --- a/services/packages/container/cleanup.go +++ b/services/packages/container/cleanup.go @@ -59,7 +59,7 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e ExactMatch: true, Value: container_model.UploadVersion, }, - IsInternal: true, + IsInternal: util.OptionalBoolTrue, HasFiles: util.OptionalBoolFalse, }) if err != nil { diff --git a/services/packages/packages.go b/services/packages/packages.go index 7f25fce5b8..0ebf6e7df0 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" @@ -451,3 +452,30 @@ func GetPackageFileStream(ctx context.Context, pf *packages_model.PackageFile) ( } return s, pf, err } + +// RemoveAllPackages for User +func RemoveAllPackages(ctx context.Context, userID int64) (int, error) { + count := 0 + for { + pkgVersions, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ + Paginator: &db.ListOptions{ + PageSize: repo_model.RepositoryListDefaultPageSize, + Page: 1, + }, + OwnerID: userID, + }) + if err != nil { + return count, fmt.Errorf("GetOwnedPackages[%d]: %w", userID, err) + } + if len(pkgVersions) == 0 { + break + } + for _, pv := range pkgVersions { + if err := DeletePackageVersionAndReferences(ctx, pv); err != nil { + return count, fmt.Errorf("unable to delete package %d:%s[%d]. Error: %w", pv.PackageID, pv.Version, pv.ID, err) + } + count++ + } + } + return count, nil +} diff --git a/services/user/user.go b/services/user/user.go index 4db4d7ca17..448b7c2daf 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -21,19 +21,116 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/avatar" + "code.gitea.io/gitea/modules/eventsource" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/packages" ) // DeleteUser completely and permanently deletes everything of a user, // but issues/comments/pulls will be kept and shown as someone has been deleted, // unless the user is younger than USER_DELETE_WITH_COMMENTS_MAX_DAYS. -func DeleteUser(u *user_model.User) error { +func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { if u.IsOrganization() { return fmt.Errorf("%s is an organization not a user", u.Name) } + if purge { + // Disable the user first + // NOTE: This is deliberately not within a transaction as it must disable the user immediately to prevent any further action by the user to be purged. + if err := user_model.UpdateUserCols(ctx, &user_model.User{ + ID: u.ID, + IsActive: false, + IsRestricted: true, + IsAdmin: false, + ProhibitLogin: true, + Passwd: "", + Salt: "", + PasswdHashAlgo: "", + MaxRepoCreation: 0, + }, "is_active", "is_restricted", "is_admin", "prohibit_login", "max_repo_creation", "passwd", "salt", "passwd_hash_algo"); err != nil { + return fmt.Errorf("unable to disable user: %s[%d] prior to purge. UpdateUserCols: %w", u.Name, u.ID, err) + } + + // Force any logged in sessions to log out + // FIXME: We also need to tell the session manager to log them out too. + eventsource.GetManager().SendMessage(u.ID, &eventsource.Event{ + Name: "logout", + }) + + // Delete all repos belonging to this user + // Now this is not within a transaction because there are internal transactions within the DeleteRepository + // BUT: the db will still be consistent even if a number of repos have already been deleted. + // And in fact we want to capture any repositories that are being created in other transactions in the meantime + // + // An alternative option here would be write a DeleteAllRepositoriesForUserID function which would delete all of the repos + // but such a function would likely get out of date + for { + repos, _, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + PageSize: repo_model.RepositoryListDefaultPageSize, + Page: 1, + }, + Private: true, + OwnerID: u.ID, + }) + if err != nil { + return fmt.Errorf("SearchRepositoryByName: %v", err) + } + if len(repos) == 0 { + break + } + for _, repo := range repos { + if err := models.DeleteRepository(u, u.ID, repo.ID); err != nil { + return fmt.Errorf("unable to delete repository %s for %s[%d]. Error: %v", repo.Name, u.Name, u.ID, err) + } + } + } + + // Remove from Organizations and delete last owner organizations + // Now this is not within a transaction because there are internal transactions within the DeleteOrganization + // BUT: the db will still be consistent even if a number of organizations memberships and organizations have already been deleted + // And in fact we want to capture any organization additions that are being created in other transactions in the meantime + // + // An alternative option here would be write a function which would delete all organizations but it seems + // but such a function would likely get out of date + for { + orgs, err := organization.FindOrgs(organization.FindOrgOptions{ + ListOptions: db.ListOptions{ + PageSize: repo_model.RepositoryListDefaultPageSize, + Page: 1, + }, + UserID: u.ID, + IncludePrivate: true, + }) + if err != nil { + return fmt.Errorf("unable to find org list for %s[%d]. Error: %v", u.Name, u.ID, err) + } + if len(orgs) == 0 { + break + } + for _, org := range orgs { + if err := models.RemoveOrgUser(org.ID, u.ID); err != nil { + if organization.IsErrLastOrgOwner(err) { + err = organization.DeleteOrganization(ctx, org) + } + if err != nil { + return fmt.Errorf("unable to remove user %s[%d] from org %s[%d]. Error: %v", u.Name, u.ID, org.Name, org.ID, err) + } + } + } + } + + // Delete Packages + if setting.Packages.Enabled { + if _, err := packages.RemoveAllPackages(ctx, u.ID); err != nil { + return err + } + } + } + ctx, committer, err := db.TxContext() if err != nil { return err @@ -41,7 +138,8 @@ func DeleteUser(u *user_model.User) error { defer committer.Close() // Note: A user owns any repository or belongs to any organization - // cannot perform delete operation. + // cannot perform delete operation. This causes a race with the purge above + // however consistency requires that we ensure that this is the case // Check ownership of repository. count, err := repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{OwnerID: u.ID}) @@ -66,7 +164,7 @@ func DeleteUser(u *user_model.User) error { return models.ErrUserOwnPackages{UID: u.ID} } - if err := models.DeleteUser(ctx, u); err != nil { + if err := models.DeleteUser(ctx, u, purge); err != nil { return fmt.Errorf("DeleteUser: %v", err) } @@ -117,7 +215,7 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error { return db.ErrCancelledf("Before delete inactive user %s", u.Name) default: } - if err := DeleteUser(u); err != nil { + if err := DeleteUser(ctx, u, false); err != nil { // Ignore users that were set inactive by admin. if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) { continue diff --git a/services/user/user_test.go b/services/user/user_test.go index cfa02b0033..aefbcd9ecb 100644 --- a/services/user/user_test.go +++ b/services/user/user_test.go @@ -33,7 +33,7 @@ func TestDeleteUser(t *testing.T) { ownedRepos := make([]*repo_model.Repository, 0, 10) assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&ownedRepos, &repo_model.Repository{OwnerID: userID})) if len(ownedRepos) > 0 { - err := DeleteUser(user) + err := DeleteUser(db.DefaultContext, user, false) assert.Error(t, err) assert.True(t, models.IsErrUserOwnRepos(err)) return @@ -47,7 +47,7 @@ func TestDeleteUser(t *testing.T) { return } } - assert.NoError(t, DeleteUser(user)) + assert.NoError(t, DeleteUser(db.DefaultContext, user, false)) unittest.AssertNotExistsBean(t, &user_model.User{ID: userID}) unittest.CheckConsistencyFor(t, &user_model.User{}, &repo_model.Repository{}) } @@ -57,7 +57,7 @@ func TestDeleteUser(t *testing.T) { test(11) org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User) - assert.Error(t, DeleteUser(org)) + assert.Error(t, DeleteUser(db.DefaultContext, org, false)) } func TestCreateUser(t *testing.T) { @@ -72,7 +72,7 @@ func TestCreateUser(t *testing.T) { assert.NoError(t, user_model.CreateUser(user)) - assert.NoError(t, DeleteUser(user)) + assert.NoError(t, DeleteUser(db.DefaultContext, user, false)) } func TestCreateUser_Issue5882(t *testing.T) { @@ -101,6 +101,6 @@ func TestCreateUser_Issue5882(t *testing.T) { assert.Equal(t, !u.AllowCreateOrganization, v.disableOrgCreation) - assert.NoError(t, DeleteUser(v.user)) + assert.NoError(t, DeleteUser(db.DefaultContext, v.user, false)) } } |