summaryrefslogtreecommitdiffstats
path: root/services
diff options
context:
space:
mode:
authorzeripath <art27@cantab.net>2022-07-14 08:22:09 +0100
committerGitHub <noreply@github.com>2022-07-14 08:22:09 +0100
commitbffa30302070b594a1c40cdc56264b9731036fb3 (patch)
tree92104ff6b8a51f5d1506742427dd1399fa42428c /services
parent175705356cac06c22d13d86b31605a6ad6dd9642 (diff)
downloadgitea-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.go2
-rw-r--r--services/packages/packages.go28
-rw-r--r--services/user/user.go106
-rw-r--r--services/user/user_test.go10
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))
}
}