Skip to content

Commit a008486

Browse files
authored
Refactor DeleteInactiveUsers, fix bug and add tests (#30206)
1. check `IsActive` before calling `IsLastAdminUser`. 2. Fix some comments and error messages. 3. Don't `return err` if "removing file" fails in `DeleteUser`. 4. Remove incorrect `DeleteInactiveEmailAddresses`. Active users could also have inactive emails, and inactive emails do not support "olderThan" 5. Add tests
1 parent 3607f82 commit a008486

File tree

3 files changed

+45
-33
lines changed

3 files changed

+45
-33
lines changed

models/user/email_address.go

-8
Original file line numberDiff line numberDiff line change
@@ -256,14 +256,6 @@ func IsEmailUsed(ctx context.Context, email string) (bool, error) {
256256
return db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(&EmailAddress{})
257257
}
258258

259-
// DeleteInactiveEmailAddresses deletes inactive email addresses
260-
func DeleteInactiveEmailAddresses(ctx context.Context) error {
261-
_, err := db.GetEngine(ctx).
262-
Where("is_activated = ?", false).
263-
Delete(new(EmailAddress))
264-
return err
265-
}
266-
267259
// ActivateEmail activates the email address to given user.
268260
func ActivateEmail(ctx context.Context, email *EmailAddress) error {
269261
ctx, committer, err := db.TxContext(ctx)

services/user/user.go

+20-25
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
126126
return fmt.Errorf("%s is an organization not a user", u.Name)
127127
}
128128

129-
if user_model.IsLastAdminUser(ctx, u) {
129+
if u.IsActive && user_model.IsLastAdminUser(ctx, u) {
130130
return models.ErrDeleteLastAdminUser{UID: u.ID}
131131
}
132132

@@ -250,7 +250,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
250250
if err := committer.Commit(); err != nil {
251251
return err
252252
}
253-
committer.Close()
253+
_ = committer.Close()
254254

255255
if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
256256
return err
@@ -259,50 +259,45 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
259259
return err
260260
}
261261

262-
// Note: There are something just cannot be roll back,
263-
// so just keep error logs of those operations.
262+
// Note: There are something just cannot be roll back, so just keep error logs of those operations.
264263
path := user_model.UserPath(u.Name)
265-
if err := util.RemoveAll(path); err != nil {
266-
err = fmt.Errorf("Failed to RemoveAll %s: %w", path, err)
264+
if err = util.RemoveAll(path); err != nil {
265+
err = fmt.Errorf("failed to RemoveAll %s: %w", path, err)
267266
_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
268-
return err
269267
}
270268

271269
if u.Avatar != "" {
272270
avatarPath := u.CustomAvatarRelativePath()
273-
if err := storage.Avatars.Delete(avatarPath); err != nil {
274-
err = fmt.Errorf("Failed to remove %s: %w", avatarPath, err)
271+
if err = storage.Avatars.Delete(avatarPath); err != nil {
272+
err = fmt.Errorf("failed to remove %s: %w", avatarPath, err)
275273
_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
276-
return err
277274
}
278275
}
279276

280277
return nil
281278
}
282279

283-
// DeleteInactiveUsers deletes all inactive users and email addresses.
280+
// DeleteInactiveUsers deletes all inactive users and their email addresses.
284281
func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
285-
users, err := user_model.GetInactiveUsers(ctx, olderThan)
282+
inactiveUsers, err := user_model.GetInactiveUsers(ctx, olderThan)
286283
if err != nil {
287284
return err
288285
}
289286

290287
// FIXME: should only update authorized_keys file once after all deletions.
291-
for _, u := range users {
292-
select {
293-
case <-ctx.Done():
294-
return db.ErrCancelledf("Before delete inactive user %s", u.Name)
295-
default:
296-
}
297-
if err := DeleteUser(ctx, u, false); err != nil {
298-
// Ignore users that were set inactive by admin.
299-
if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) ||
300-
models.IsErrUserOwnPackages(err) || models.IsErrDeleteLastAdminUser(err) {
288+
for _, u := range inactiveUsers {
289+
if err = DeleteUser(ctx, u, false); err != nil {
290+
// Ignore inactive users that were ever active but then were set inactive by admin
291+
if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) {
301292
continue
302293
}
303-
return err
294+
select {
295+
case <-ctx.Done():
296+
return db.ErrCancelledf("when deleting inactive user %q", u.Name)
297+
default:
298+
return err
299+
}
304300
}
305301
}
306-
307-
return user_model.DeleteInactiveEmailAddresses(ctx)
302+
return nil // TODO: there could be still inactive users left, and the number would increase gradually
308303
}

services/user/user_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"strings"
99
"testing"
10+
"time"
1011

1112
"code.gitea.io/gitea/models"
1213
"code.gitea.io/gitea/models/auth"
@@ -16,6 +17,7 @@ import (
1617
"code.gitea.io/gitea/models/unittest"
1718
user_model "code.gitea.io/gitea/models/user"
1819
"code.gitea.io/gitea/modules/setting"
20+
"code.gitea.io/gitea/modules/timeutil"
1921

2022
"github.com/stretchr/testify/assert"
2123
)
@@ -185,3 +187,26 @@ func TestCreateUser_Issue5882(t *testing.T) {
185187
assert.NoError(t, DeleteUser(db.DefaultContext, v.user, false))
186188
}
187189
}
190+
191+
func TestDeleteInactiveUsers(t *testing.T) {
192+
addUser := func(name, email string, createdUnix timeutil.TimeStamp, active bool) {
193+
inactiveUser := &user_model.User{Name: name, LowerName: strings.ToLower(name), Email: email, CreatedUnix: createdUnix, IsActive: active}
194+
_, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(inactiveUser)
195+
assert.NoError(t, err)
196+
inactiveUserEmail := &user_model.EmailAddress{UID: inactiveUser.ID, IsPrimary: true, Email: email, LowerEmail: strings.ToLower(email), IsActivated: active}
197+
err = db.Insert(db.DefaultContext, inactiveUserEmail)
198+
assert.NoError(t, err)
199+
}
200+
addUser("user-inactive-10", "[email protected]", timeutil.TimeStampNow().Add(-600), false)
201+
addUser("user-inactive-5", "[email protected]", timeutil.TimeStampNow().Add(-300), false)
202+
addUser("user-active-10", "[email protected]", timeutil.TimeStampNow().Add(-600), true)
203+
addUser("user-active-5", "[email protected]", timeutil.TimeStampNow().Add(-300), true)
204+
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-inactive-10"})
205+
unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "[email protected]"})
206+
assert.NoError(t, DeleteInactiveUsers(db.DefaultContext, 8*time.Minute))
207+
unittest.AssertNotExistsBean(t, &user_model.User{Name: "user-inactive-10"})
208+
unittest.AssertNotExistsBean(t, &user_model.EmailAddress{Email: "[email protected]"})
209+
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-inactive-5"})
210+
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-active-10"})
211+
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-active-5"})
212+
}

0 commit comments

Comments
 (0)