Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f34151b

Browse files
authoredNov 18, 2021
Move user/org deletion to services (#17673)
1 parent 55be5fe commit f34151b

File tree

24 files changed

+382
-301
lines changed

24 files changed

+382
-301
lines changed
 

‎cmd/admin.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
auth_service "code.gitea.io/gitea/services/auth"
2727
"code.gitea.io/gitea/services/auth/source/oauth2"
2828
repo_service "code.gitea.io/gitea/services/repository"
29+
user_service "code.gitea.io/gitea/services/user"
2930

3031
"github.com/urfave/cli"
3132
)
@@ -534,7 +535,7 @@ func runDeleteUser(c *cli.Context) error {
534535
return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id"))
535536
}
536537

537-
return models.DeleteUser(user)
538+
return user_service.DeleteUser(user)
538539
}
539540

540541
func runRepoSyncReleases(_ *cli.Context) error {

‎models/admin/notice.go

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,7 @@ func (n *Notice) TrStr() string {
4343
}
4444

4545
// CreateNotice creates new system notice.
46-
func CreateNotice(tp NoticeType, desc string, args ...interface{}) error {
47-
return CreateNoticeCtx(db.DefaultContext, tp, desc, args...)
48-
}
49-
50-
// CreateNoticeCtx creates new system notice.
51-
func CreateNoticeCtx(ctx context.Context, tp NoticeType, desc string, args ...interface{}) error {
46+
func CreateNotice(ctx context.Context, tp NoticeType, desc string, args ...interface{}) error {
5247
if len(args) > 0 {
5348
desc = fmt.Sprintf(desc, args...)
5449
}
@@ -61,38 +56,28 @@ func CreateNoticeCtx(ctx context.Context, tp NoticeType, desc string, args ...in
6156

6257
// CreateRepositoryNotice creates new system notice with type NoticeRepository.
6358
func CreateRepositoryNotice(desc string, args ...interface{}) error {
64-
return CreateNoticeCtx(db.DefaultContext, NoticeRepository, desc, args...)
59+
return CreateNotice(db.DefaultContext, NoticeRepository, desc, args...)
6560
}
6661

6762
// RemoveAllWithNotice removes all directories in given path and
6863
// creates a system notice when error occurs.
69-
func RemoveAllWithNotice(title, path string) {
70-
RemoveAllWithNoticeCtx(db.DefaultContext, title, path)
71-
}
72-
73-
// RemoveStorageWithNotice removes a file from the storage and
74-
// creates a system notice when error occurs.
75-
func RemoveStorageWithNotice(bucket storage.ObjectStorage, title, path string) {
76-
removeStorageWithNotice(db.DefaultContext, bucket, title, path)
77-
}
78-
79-
func removeStorageWithNotice(ctx context.Context, bucket storage.ObjectStorage, title, path string) {
80-
if err := bucket.Delete(path); err != nil {
64+
func RemoveAllWithNotice(ctx context.Context, title, path string) {
65+
if err := util.RemoveAll(path); err != nil {
8166
desc := fmt.Sprintf("%s [%s]: %v", title, path, err)
8267
log.Warn(title+" [%s]: %v", path, err)
83-
if err = CreateNoticeCtx(ctx, NoticeRepository, desc); err != nil {
68+
if err = CreateNotice(ctx, NoticeRepository, desc); err != nil {
8469
log.Error("CreateRepositoryNotice: %v", err)
8570
}
8671
}
8772
}
8873

89-
// RemoveAllWithNoticeCtx removes all directories in given path and
74+
// RemoveStorageWithNotice removes a file from the storage and
9075
// creates a system notice when error occurs.
91-
func RemoveAllWithNoticeCtx(ctx context.Context, title, path string) {
92-
if err := util.RemoveAll(path); err != nil {
76+
func RemoveStorageWithNotice(ctx context.Context, bucket storage.ObjectStorage, title, path string) {
77+
if err := bucket.Delete(path); err != nil {
9378
desc := fmt.Sprintf("%s [%s]: %v", title, path, err)
9479
log.Warn(title+" [%s]: %v", path, err)
95-
if err = CreateNoticeCtx(ctx, NoticeRepository, desc); err != nil {
80+
if err = CreateNotice(ctx, NoticeRepository, desc); err != nil {
9681
log.Error("CreateRepositoryNotice: %v", err)
9782
}
9883
}

‎models/admin/notice_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package admin
77
import (
88
"testing"
99

10+
"code.gitea.io/gitea/models/db"
1011
"code.gitea.io/gitea/models/unittest"
1112

1213
"github.com/stretchr/testify/assert"
@@ -28,7 +29,7 @@ func TestCreateNotice(t *testing.T) {
2829
Description: "test description",
2930
}
3031
unittest.AssertNotExistsBean(t, noticeBean)
31-
assert.NoError(t, CreateNotice(noticeBean.Type, noticeBean.Description))
32+
assert.NoError(t, CreateNotice(db.DefaultContext, noticeBean.Type, noticeBean.Description))
3233
unittest.AssertExistsAndLoadBean(t, noticeBean)
3334
}
3435

‎models/consistency.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ func DeleteOrphanedIssues() error {
128128

129129
// Remove issue attachment files.
130130
for i := range attachmentPaths {
131-
admin_model.RemoveAllWithNoticeCtx(db.DefaultContext, "Delete issue attachment", attachmentPaths[i])
131+
admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete issue attachment", attachmentPaths[i])
132132
}
133133
return nil
134134
}

‎models/org.go

Lines changed: 9 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package models
77

88
import (
9+
"context"
910
"fmt"
1011
"strings"
1112

@@ -14,9 +15,7 @@ import (
1415
user_model "code.gitea.io/gitea/models/user"
1516
"code.gitea.io/gitea/modules/log"
1617
"code.gitea.io/gitea/modules/setting"
17-
"code.gitea.io/gitea/modules/storage"
1818
"code.gitea.io/gitea/modules/structs"
19-
"code.gitea.io/gitea/modules/util"
2019

2120
"xorm.io/builder"
2221
"xorm.io/xorm"
@@ -254,68 +253,23 @@ func CountOrganizations() int64 {
254253
return count
255254
}
256255

257-
// DeleteOrganization completely and permanently deletes everything of organization.
258-
func DeleteOrganization(org *User) (err error) {
259-
if !org.IsOrganization() {
260-
return fmt.Errorf("%s is a user not an organization", org.Name)
261-
}
262-
263-
sess := db.NewSession(db.DefaultContext)
264-
defer sess.Close()
265-
266-
if err = sess.Begin(); err != nil {
267-
return err
268-
}
269-
270-
if err = deleteOrg(sess, org); err != nil {
271-
if IsErrUserOwnRepos(err) {
272-
return err
273-
} else if err != nil {
274-
return fmt.Errorf("deleteOrg: %v", err)
275-
}
276-
}
277-
278-
return sess.Commit()
279-
}
280-
281-
func deleteOrg(e *xorm.Session, u *User) error {
282-
// Check ownership of repository.
283-
count, err := getRepositoryCount(e, u)
284-
if err != nil {
285-
return fmt.Errorf("GetRepositoryCount: %v", err)
286-
} else if count > 0 {
287-
return ErrUserOwnRepos{UID: u.ID}
288-
}
256+
// DeleteOrganization deletes models associated to an organization.
257+
func DeleteOrganization(ctx context.Context, org *User) error {
258+
e := db.GetEngine(ctx)
289259

290260
if err := deleteBeans(e,
291-
&Team{OrgID: u.ID},
292-
&OrgUser{OrgID: u.ID},
293-
&TeamUser{OrgID: u.ID},
294-
&TeamUnit{OrgID: u.ID},
261+
&Team{OrgID: org.ID},
262+
&OrgUser{OrgID: org.ID},
263+
&TeamUser{OrgID: org.ID},
264+
&TeamUnit{OrgID: org.ID},
295265
); err != nil {
296266
return fmt.Errorf("deleteBeans: %v", err)
297267
}
298268

299-
if _, err = e.ID(u.ID).Delete(new(User)); err != nil {
269+
if _, err := e.ID(org.ID).Delete(new(User)); err != nil {
300270
return fmt.Errorf("Delete: %v", err)
301271
}
302272

303-
// FIXME: system notice
304-
// Note: There are something just cannot be roll back,
305-
// so just keep error logs of those operations.
306-
path := UserPath(u.Name)
307-
308-
if err := util.RemoveAll(path); err != nil {
309-
return fmt.Errorf("Failed to RemoveAll %s: %v", path, err)
310-
}
311-
312-
if len(u.Avatar) > 0 {
313-
avatarPath := u.CustomAvatarRelativePath()
314-
if err := storage.Avatars.Delete(avatarPath); err != nil {
315-
return fmt.Errorf("Failed to remove %s: %v", avatarPath, err)
316-
}
317-
}
318-
319273
return nil
320274
}
321275

‎models/org_test.go

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -261,24 +261,6 @@ func TestCountOrganizations(t *testing.T) {
261261
assert.Equal(t, expected, CountOrganizations())
262262
}
263263

264-
func TestDeleteOrganization(t *testing.T) {
265-
assert.NoError(t, unittest.PrepareTestDatabase())
266-
org := unittest.AssertExistsAndLoadBean(t, &User{ID: 6}).(*User)
267-
assert.NoError(t, DeleteOrganization(org))
268-
unittest.AssertNotExistsBean(t, &User{ID: 6})
269-
unittest.AssertNotExistsBean(t, &OrgUser{OrgID: 6})
270-
unittest.AssertNotExistsBean(t, &Team{OrgID: 6})
271-
272-
org = unittest.AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
273-
err := DeleteOrganization(org)
274-
assert.Error(t, err)
275-
assert.True(t, IsErrUserOwnRepos(err))
276-
277-
user := unittest.AssertExistsAndLoadBean(t, &User{ID: 5}).(*User)
278-
assert.Error(t, DeleteOrganization(user))
279-
unittest.CheckConsistencyFor(t, &User{}, &Team{})
280-
}
281-
282264
func TestIsOrganizationOwner(t *testing.T) {
283265
assert.NoError(t, unittest.PrepareTestDatabase())
284266
test := func(orgID, userID int64, expected bool) {

‎models/repo.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func NewRepoContext() {
134134
loadRepoConfig()
135135
unit.LoadUnitConfig()
136136

137-
admin_model.RemoveAllWithNotice("Clean up repository temporary data", filepath.Join(setting.AppDataPath, "tmp"))
137+
admin_model.RemoveAllWithNotice(db.DefaultContext, "Clean up repository temporary data", filepath.Join(setting.AppDataPath, "tmp"))
138138
}
139139

140140
// RepositoryStatus defines the status of repository
@@ -1649,36 +1649,36 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
16491649

16501650
// Remove repository files.
16511651
repoPath := repo.RepoPath()
1652-
admin_model.RemoveAllWithNotice("Delete repository files", repoPath)
1652+
admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository files", repoPath)
16531653

16541654
// Remove wiki files
16551655
if repo.HasWiki() {
1656-
admin_model.RemoveAllWithNotice("Delete repository wiki", repo.WikiPath())
1656+
admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository wiki", repo.WikiPath())
16571657
}
16581658

16591659
// Remove archives
16601660
for i := range archivePaths {
1661-
admin_model.RemoveStorageWithNotice(storage.RepoArchives, "Delete repo archive file", archivePaths[i])
1661+
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archivePaths[i])
16621662
}
16631663

16641664
// Remove lfs objects
16651665
for i := range lfsPaths {
1666-
admin_model.RemoveStorageWithNotice(storage.LFS, "Delete orphaned LFS file", lfsPaths[i])
1666+
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsPaths[i])
16671667
}
16681668

16691669
// Remove issue attachment files.
16701670
for i := range attachmentPaths {
1671-
admin_model.RemoveStorageWithNotice(storage.Attachments, "Delete issue attachment", attachmentPaths[i])
1671+
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachmentPaths[i])
16721672
}
16731673

16741674
// Remove release attachment files.
16751675
for i := range releaseAttachments {
1676-
admin_model.RemoveStorageWithNotice(storage.Attachments, "Delete release attachment", releaseAttachments[i])
1676+
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachments[i])
16771677
}
16781678

16791679
// Remove attachment with no issue_id and release_id.
16801680
for i := range newAttachmentPaths {
1681-
admin_model.RemoveStorageWithNotice(storage.Attachments, "Delete issue attachment", attachmentPaths[i])
1681+
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachmentPaths[i])
16821682
}
16831683

16841684
if len(repo.Avatar) > 0 {
@@ -1803,8 +1803,8 @@ func getPrivateRepositoryCount(e db.Engine, u *User) (int64, error) {
18031803
}
18041804

18051805
// GetRepositoryCount returns the total number of repositories of user.
1806-
func GetRepositoryCount(u *User) (int64, error) {
1807-
return getRepositoryCount(db.GetEngine(db.DefaultContext), u)
1806+
func GetRepositoryCount(ctx context.Context, u *User) (int64, error) {
1807+
return getRepositoryCount(db.GetEngine(ctx), u)
18081808
}
18091809

18101810
// GetPublicRepositoryCount returns the total number of public repositories of user.

‎models/repo_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func TestMetas(t *testing.T) {
7171
func TestGetRepositoryCount(t *testing.T) {
7272
assert.NoError(t, unittest.PrepareTestDatabase())
7373

74-
count, err1 := GetRepositoryCount(&User{ID: int64(10)})
74+
count, err1 := GetRepositoryCount(db.DefaultContext, &User{ID: int64(10)})
7575
privateCount, err2 := GetPrivateRepositoryCount(&User{ID: int64(10)})
7676
publicCount, err3 := GetPublicRepositoryCount(&User{ID: int64(10)})
7777
assert.NoError(t, err1)

‎models/user.go

Lines changed: 14 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222

2323
_ "image/jpeg" // Needed for jpeg support
2424

25-
admin_model "code.gitea.io/gitea/models/admin"
2625
"code.gitea.io/gitea/models/db"
2726
"code.gitea.io/gitea/models/login"
2827
"code.gitea.io/gitea/models/unit"
@@ -32,7 +31,6 @@ import (
3231
"code.gitea.io/gitea/modules/git"
3332
"code.gitea.io/gitea/modules/log"
3433
"code.gitea.io/gitea/modules/setting"
35-
"code.gitea.io/gitea/modules/storage"
3634
"code.gitea.io/gitea/modules/structs"
3735
"code.gitea.io/gitea/modules/timeutil"
3836
"code.gitea.io/gitea/modules/util"
@@ -542,15 +540,16 @@ func (u *User) IsPublicMember(orgID int64) bool {
542540
return isMember
543541
}
544542

545-
func (u *User) getOrganizationCount(e db.Engine) (int64, error) {
546-
return e.
543+
// GetOrganizationCount returns count of membership of organization of the user.
544+
func GetOrganizationCount(ctx context.Context, u *User) (int64, error) {
545+
return db.GetEngine(ctx).
547546
Where("uid=?", u.ID).
548547
Count(new(OrgUser))
549548
}
550549

551550
// GetOrganizationCount returns count of membership of organization of user.
552551
func (u *User) GetOrganizationCount() (int64, error) {
553-
return u.getOrganizationCount(db.GetEngine(db.DefaultContext))
552+
return GetOrganizationCount(db.DefaultContext, u)
554553
}
555554

556555
// GetRepositories returns repositories that user owns, including private repositories.
@@ -1149,26 +1148,9 @@ func deleteBeans(e db.Engine, beans ...interface{}) (err error) {
11491148
return nil
11501149
}
11511150

1152-
func deleteUser(ctx context.Context, u *User) error {
1151+
// DeleteUser deletes models associated to an user.
1152+
func DeleteUser(ctx context.Context, u *User) (err error) {
11531153
e := db.GetEngine(ctx)
1154-
// Note: A user owns any repository or belongs to any organization
1155-
// cannot perform delete operation.
1156-
1157-
// Check ownership of repository.
1158-
count, err := getRepositoryCount(e, u)
1159-
if err != nil {
1160-
return fmt.Errorf("GetRepositoryCount: %v", err)
1161-
} else if count > 0 {
1162-
return ErrUserOwnRepos{UID: u.ID}
1163-
}
1164-
1165-
// Check membership of organization.
1166-
count, err = u.getOrganizationCount(e)
1167-
if err != nil {
1168-
return fmt.Errorf("GetOrganizationCount: %v", err)
1169-
} else if count > 0 {
1170-
return ErrUserHasOrgs{UID: u.ID}
1171-
}
11721154

11731155
// ***** START: Watch *****
11741156
watchedRepoIDs := make([]int64, 0, 10)
@@ -1301,85 +1283,21 @@ func deleteUser(ctx context.Context, u *User) error {
13011283
return fmt.Errorf("Delete: %v", err)
13021284
}
13031285

1304-
// Note: There are something just cannot be roll back,
1305-
// so just keep error logs of those operations.
1306-
path := UserPath(u.Name)
1307-
if err = util.RemoveAll(path); err != nil {
1308-
err = fmt.Errorf("Failed to RemoveAll %s: %v", path, err)
1309-
_ = admin_model.CreateNoticeCtx(ctx, admin_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
1310-
return err
1311-
}
1312-
1313-
if len(u.Avatar) > 0 {
1314-
avatarPath := u.CustomAvatarRelativePath()
1315-
if err = storage.Avatars.Delete(avatarPath); err != nil {
1316-
err = fmt.Errorf("Failed to remove %s: %v", avatarPath, err)
1317-
_ = admin_model.CreateNoticeCtx(ctx, admin_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
1318-
return err
1319-
}
1320-
}
1321-
13221286
return nil
13231287
}
13241288

1325-
// DeleteUser completely and permanently deletes everything of a user,
1326-
// but issues/comments/pulls will be kept and shown as someone has been deleted,
1327-
// unless the user is younger than USER_DELETE_WITH_COMMENTS_MAX_DAYS.
1328-
func DeleteUser(u *User) (err error) {
1329-
if u.IsOrganization() {
1330-
return fmt.Errorf("%s is an organization not a user", u.Name)
1331-
}
1332-
1333-
ctx, committer, err := db.TxContext()
1334-
if err != nil {
1335-
return err
1336-
}
1337-
defer committer.Close()
1338-
1339-
if err = deleteUser(ctx, u); err != nil {
1340-
// Note: don't wrapper error here.
1341-
return err
1342-
}
1343-
1344-
return committer.Commit()
1345-
}
1289+
// GetInactiveUsers gets all inactive users
1290+
func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, error) {
1291+
var cond builder.Cond = builder.Eq{"is_active": false}
13461292

1347-
// DeleteInactiveUsers deletes all inactive users and email addresses.
1348-
func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) (err error) {
1349-
users := make([]*User, 0, 10)
13501293
if olderThan > 0 {
1351-
if err = db.GetEngine(db.DefaultContext).
1352-
Where("is_active = ? and created_unix < ?", false, time.Now().Add(-olderThan).Unix()).
1353-
Find(&users); err != nil {
1354-
return fmt.Errorf("get all inactive users: %v", err)
1355-
}
1356-
} else {
1357-
if err = db.GetEngine(db.DefaultContext).
1358-
Where("is_active = ?", false).
1359-
Find(&users); err != nil {
1360-
return fmt.Errorf("get all inactive users: %v", err)
1361-
}
1362-
}
1363-
// FIXME: should only update authorized_keys file once after all deletions.
1364-
for _, u := range users {
1365-
select {
1366-
case <-ctx.Done():
1367-
return db.ErrCancelledf("Before delete inactive user %s", u.Name)
1368-
default:
1369-
}
1370-
if err = DeleteUser(u); err != nil {
1371-
// Ignore users that were set inactive by admin.
1372-
if IsErrUserOwnRepos(err) || IsErrUserHasOrgs(err) {
1373-
continue
1374-
}
1375-
return err
1376-
}
1294+
cond = cond.And(builder.Lt{"created_unix": time.Now().Add(-olderThan).Unix()})
13771295
}
13781296

1379-
_, err = db.GetEngine(db.DefaultContext).
1380-
Where("is_activated = ?", false).
1381-
Delete(new(user_model.EmailAddress))
1382-
return err
1297+
users := make([]*User, 0, 10)
1298+
return users, db.GetEngine(ctx).
1299+
Where(cond).
1300+
Find(&users)
13831301
}
13841302

13851303
// UserPath returns the path absolute path of user repositories.

‎models/user/email_address.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,11 @@ func DeleteEmailAddresses(emails []*EmailAddress) (err error) {
267267

268268
return nil
269269
}
270+
271+
// DeleteInactiveEmailAddresses deletes inactive email addresses
272+
func DeleteInactiveEmailAddresses(ctx context.Context) error {
273+
_, err := db.GetEngine(ctx).
274+
Where("is_activated = ?", false).
275+
Delete(new(EmailAddress))
276+
return err
277+
}

‎models/user_test.go

Lines changed: 0 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -177,41 +177,6 @@ func TestSearchUsers(t *testing.T) {
177177
[]int64{24})
178178
}
179179

180-
func TestDeleteUser(t *testing.T) {
181-
test := func(userID int64) {
182-
assert.NoError(t, unittest.PrepareTestDatabase())
183-
user := unittest.AssertExistsAndLoadBean(t, &User{ID: userID}).(*User)
184-
185-
ownedRepos := make([]*Repository, 0, 10)
186-
assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&ownedRepos, &Repository{OwnerID: userID}))
187-
if len(ownedRepos) > 0 {
188-
err := DeleteUser(user)
189-
assert.Error(t, err)
190-
assert.True(t, IsErrUserOwnRepos(err))
191-
return
192-
}
193-
194-
orgUsers := make([]*OrgUser, 0, 10)
195-
assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &OrgUser{UID: userID}))
196-
for _, orgUser := range orgUsers {
197-
if err := RemoveOrgUser(orgUser.OrgID, orgUser.UID); err != nil {
198-
assert.True(t, IsErrLastOrgOwner(err))
199-
return
200-
}
201-
}
202-
assert.NoError(t, DeleteUser(user))
203-
unittest.AssertNotExistsBean(t, &User{ID: userID})
204-
unittest.CheckConsistencyFor(t, &User{}, &Repository{})
205-
}
206-
test(2)
207-
test(4)
208-
test(8)
209-
test(11)
210-
211-
org := unittest.AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
212-
assert.Error(t, DeleteUser(org))
213-
}
214-
215180
func TestEmailNotificationPreferences(t *testing.T) {
216181
assert.NoError(t, unittest.PrepareTestDatabase())
217182

@@ -333,21 +298,6 @@ func TestDisplayName(t *testing.T) {
333298
}
334299
}
335300

336-
func TestCreateUser(t *testing.T) {
337-
user := &User{
338-
Name: "GiteaBot",
339-
Email: "GiteaBot@gitea.io",
340-
Passwd: ";p['////..-++']",
341-
IsAdmin: false,
342-
Theme: setting.UI.DefaultTheme,
343-
MustChangePassword: false,
344-
}
345-
346-
assert.NoError(t, CreateUser(user))
347-
348-
assert.NoError(t, DeleteUser(user))
349-
}
350-
351301
func TestCreateUserInvalidEmail(t *testing.T) {
352302
user := &User{
353303
Name: "GiteaBot",
@@ -363,36 +313,6 @@ func TestCreateUserInvalidEmail(t *testing.T) {
363313
assert.True(t, user_model.IsErrEmailInvalid(err))
364314
}
365315

366-
func TestCreateUser_Issue5882(t *testing.T) {
367-
// Init settings
368-
_ = setting.Admin
369-
370-
passwd := ".//.;1;;//.,-=_"
371-
372-
tt := []struct {
373-
user *User
374-
disableOrgCreation bool
375-
}{
376-
{&User{Name: "GiteaBot", Email: "GiteaBot@gitea.io", Passwd: passwd, MustChangePassword: false}, false},
377-
{&User{Name: "GiteaBot2", Email: "GiteaBot2@gitea.io", Passwd: passwd, MustChangePassword: false}, true},
378-
}
379-
380-
setting.Service.DefaultAllowCreateOrganization = true
381-
382-
for _, v := range tt {
383-
setting.Admin.DisableRegularOrgCreation = v.disableOrgCreation
384-
385-
assert.NoError(t, CreateUser(v.user))
386-
387-
u, err := GetUserByEmail(v.user.Email)
388-
assert.NoError(t, err)
389-
390-
assert.Equal(t, !u.AllowCreateOrganization, v.disableOrgCreation)
391-
392-
assert.NoError(t, DeleteUser(v.user))
393-
}
394-
}
395-
396316
func TestGetUserIDsByNames(t *testing.T) {
397317
assert.NoError(t, unittest.PrepareTestDatabase())
398318

‎modules/repository/create_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010

1111
"code.gitea.io/gitea/models"
12+
"code.gitea.io/gitea/models/db"
1213
"code.gitea.io/gitea/models/unittest"
1314
"code.gitea.io/gitea/modules/structs"
1415

@@ -142,5 +143,5 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
142143
assert.NoError(t, models.DeleteRepository(user, org.ID, rid), "DeleteRepository %d", i)
143144
}
144145
}
145-
assert.NoError(t, models.DeleteOrganization(org), "DeleteOrganization")
146+
assert.NoError(t, models.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization")
146147
}

‎routers/api/v1/admin/user.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"code.gitea.io/gitea/routers/api/v1/user"
2323
"code.gitea.io/gitea/routers/api/v1/utils"
2424
"code.gitea.io/gitea/services/mailer"
25+
user_service "code.gitea.io/gitea/services/user"
2526
)
2627

2728
func parseLoginSource(ctx *context.APIContext, u *models.User, sourceID int64, loginName string) {
@@ -289,7 +290,7 @@ func DeleteUser(ctx *context.APIContext) {
289290
return
290291
}
291292

292-
if err := models.DeleteUser(u); err != nil {
293+
if err := user_service.DeleteUser(u); err != nil {
293294
if models.IsErrUserOwnRepos(err) ||
294295
models.IsErrUserHasOrgs(err) {
295296
ctx.Error(http.StatusUnprocessableEntity, "", err)

‎routers/api/v1/org/org.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"code.gitea.io/gitea/modules/web"
1717
"code.gitea.io/gitea/routers/api/v1/user"
1818
"code.gitea.io/gitea/routers/api/v1/utils"
19+
"code.gitea.io/gitea/services/org"
1920
)
2021

2122
func listUserOrgs(ctx *context.APIContext, u *models.User) {
@@ -364,7 +365,7 @@ func Delete(ctx *context.APIContext) {
364365
// "204":
365366
// "$ref": "#/responses/empty"
366367

367-
if err := models.DeleteOrganization(ctx.Org.Organization); err != nil {
368+
if err := org.DeleteOrganization(ctx.Org.Organization); err != nil {
368369
ctx.Error(http.StatusInternalServerError, "DeleteOrganization", err)
369370
return
370371
}

‎routers/web/admin/users.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
router_user_setting "code.gitea.io/gitea/routers/web/user/setting"
2727
"code.gitea.io/gitea/services/forms"
2828
"code.gitea.io/gitea/services/mailer"
29+
"code.gitea.io/gitea/services/user"
2930
)
3031

3132
const (
@@ -377,7 +378,7 @@ func DeleteUser(ctx *context.Context) {
377378
return
378379
}
379380

380-
if err = models.DeleteUser(u); err != nil {
381+
if err = user.DeleteUser(u); err != nil {
381382
switch {
382383
case models.IsErrUserOwnRepos(err):
383384
ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo"))

‎routers/web/org/setting.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"code.gitea.io/gitea/modules/web"
2121
userSetting "code.gitea.io/gitea/routers/web/user/setting"
2222
"code.gitea.io/gitea/services/forms"
23+
"code.gitea.io/gitea/services/org"
2324
)
2425

2526
const (
@@ -156,23 +157,22 @@ func SettingsDelete(ctx *context.Context) {
156157
ctx.Data["Title"] = ctx.Tr("org.settings")
157158
ctx.Data["PageIsSettingsDelete"] = true
158159

159-
org := ctx.Org.Organization
160160
if ctx.Req.Method == "POST" {
161-
if org.Name != ctx.FormString("org_name") {
161+
if ctx.Org.Organization.Name != ctx.FormString("org_name") {
162162
ctx.Data["Err_OrgName"] = true
163163
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil)
164164
return
165165
}
166166

167-
if err := models.DeleteOrganization(org); err != nil {
167+
if err := org.DeleteOrganization(ctx.Org.Organization); err != nil {
168168
if models.IsErrUserOwnRepos(err) {
169169
ctx.Flash.Error(ctx.Tr("form.org_still_own_repo"))
170170
ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
171171
} else {
172172
ctx.ServerError("DeleteOrganization", err)
173173
}
174174
} else {
175-
log.Trace("Organization deleted: %s", org.Name)
175+
log.Trace("Organization deleted: %s", ctx.Org.Organization.Name)
176176
ctx.Redirect(setting.AppSubURL + "/")
177177
}
178178
return

‎routers/web/user/setting/account.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"code.gitea.io/gitea/services/auth"
2323
"code.gitea.io/gitea/services/forms"
2424
"code.gitea.io/gitea/services/mailer"
25+
"code.gitea.io/gitea/services/user"
2526
)
2627

2728
const (
@@ -241,7 +242,7 @@ func DeleteAccount(ctx *context.Context) {
241242
return
242243
}
243244

244-
if err := models.DeleteUser(ctx.User); err != nil {
245+
if err := user.DeleteUser(ctx.User); err != nil {
245246
switch {
246247
case models.IsErrUserOwnRepos(err):
247248
ctx.Flash.Error(ctx.Tr("form.still_own_repo"))

‎services/cron/tasks.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,18 @@ func (t *Task) RunWithUser(doer *models.User, config Config) {
9090
if err := t.fun(ctx, doer, config); err != nil {
9191
if db.IsErrCancelled(err) {
9292
message := err.(db.ErrCancelled).Message
93-
if err := admin_model.CreateNotice(admin_model.NoticeTask, config.FormatMessage(t.Name, "aborted", doer, message)); err != nil {
93+
if err := admin_model.CreateNotice(ctx, admin_model.NoticeTask, config.FormatMessage(t.Name, "aborted", doer, message)); err != nil {
9494
log.Error("CreateNotice: %v", err)
9595
}
9696
return
9797
}
98-
if err := admin_model.CreateNotice(admin_model.NoticeTask, config.FormatMessage(t.Name, "error", doer, err)); err != nil {
98+
if err := admin_model.CreateNotice(ctx, admin_model.NoticeTask, config.FormatMessage(t.Name, "error", doer, err)); err != nil {
9999
log.Error("CreateNotice: %v", err)
100100
}
101101
return
102102
}
103103
if config.DoNoticeOnSuccess() {
104-
if err := admin_model.CreateNotice(admin_model.NoticeTask, config.FormatMessage(t.Name, "finished", doer)); err != nil {
104+
if err := admin_model.CreateNotice(ctx, admin_model.NoticeTask, config.FormatMessage(t.Name, "finished", doer)); err != nil {
105105
log.Error("CreateNotice: %v", err)
106106
}
107107
}

‎services/cron/tasks_extended.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"code.gitea.io/gitea/modules/setting"
1414
"code.gitea.io/gitea/modules/updatechecker"
1515
repo_service "code.gitea.io/gitea/services/repository"
16+
user_service "code.gitea.io/gitea/services/user"
1617
)
1718

1819
func registerDeleteInactiveUsers() {
@@ -25,7 +26,7 @@ func registerDeleteInactiveUsers() {
2526
OlderThan: 0 * time.Second,
2627
}, func(ctx context.Context, _ *models.User, config Config) error {
2728
olderThanConfig := config.(*OlderThanConfig)
28-
return models.DeleteInactiveUsers(ctx, olderThanConfig.OlderThan)
29+
return user_service.DeleteInactiveUsers(ctx, olderThanConfig.OlderThan)
2930
})
3031
}
3132

‎services/org/org.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package org
6+
7+
import (
8+
"fmt"
9+
10+
"code.gitea.io/gitea/models"
11+
"code.gitea.io/gitea/models/db"
12+
"code.gitea.io/gitea/modules/storage"
13+
"code.gitea.io/gitea/modules/util"
14+
)
15+
16+
// DeleteOrganization completely and permanently deletes everything of organization.
17+
func DeleteOrganization(org *models.User) error {
18+
if !org.IsOrganization() {
19+
return fmt.Errorf("%s is a user not an organization", org.Name)
20+
}
21+
22+
ctx, commiter, err := db.TxContext()
23+
if err != nil {
24+
return err
25+
}
26+
defer commiter.Close()
27+
28+
// Check ownership of repository.
29+
count, err := models.GetRepositoryCount(ctx, org)
30+
if err != nil {
31+
return fmt.Errorf("GetRepositoryCount: %v", err)
32+
} else if count > 0 {
33+
return models.ErrUserOwnRepos{UID: org.ID}
34+
}
35+
36+
if err := models.DeleteOrganization(ctx, org); err != nil {
37+
return fmt.Errorf("DeleteOrganization: %v", err)
38+
}
39+
40+
if err := commiter.Commit(); err != nil {
41+
return err
42+
}
43+
44+
// FIXME: system notice
45+
// Note: There are something just cannot be roll back,
46+
// so just keep error logs of those operations.
47+
path := models.UserPath(org.Name)
48+
49+
if err := util.RemoveAll(path); err != nil {
50+
return fmt.Errorf("Failed to RemoveAll %s: %v", path, err)
51+
}
52+
53+
if len(org.Avatar) > 0 {
54+
avatarPath := org.CustomAvatarRelativePath()
55+
if err := storage.Avatars.Delete(avatarPath); err != nil {
56+
return fmt.Errorf("Failed to remove %s: %v", avatarPath, err)
57+
}
58+
}
59+
60+
return nil
61+
}

‎services/org/org_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package org
6+
7+
import (
8+
"path/filepath"
9+
"testing"
10+
11+
"code.gitea.io/gitea/models"
12+
"code.gitea.io/gitea/models/unittest"
13+
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestMain(m *testing.M) {
18+
unittest.MainTest(m, filepath.Join("..", ".."))
19+
}
20+
21+
func TestDeleteOrganization(t *testing.T) {
22+
assert.NoError(t, unittest.PrepareTestDatabase())
23+
org := unittest.AssertExistsAndLoadBean(t, &models.User{ID: 6}).(*models.User)
24+
assert.NoError(t, DeleteOrganization(org))
25+
unittest.AssertNotExistsBean(t, &models.User{ID: 6})
26+
unittest.AssertNotExistsBean(t, &models.OrgUser{OrgID: 6})
27+
unittest.AssertNotExistsBean(t, &models.Team{OrgID: 6})
28+
29+
org = unittest.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User)
30+
err := DeleteOrganization(org)
31+
assert.Error(t, err)
32+
assert.True(t, models.IsErrUserOwnRepos(err))
33+
34+
user := unittest.AssertExistsAndLoadBean(t, &models.User{ID: 5}).(*models.User)
35+
assert.Error(t, DeleteOrganization(user))
36+
unittest.CheckConsistencyFor(t, &models.User{}, &models.Team{})
37+
}

‎services/user/user.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package user
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"time"
11+
12+
"code.gitea.io/gitea/models"
13+
admin_model "code.gitea.io/gitea/models/admin"
14+
"code.gitea.io/gitea/models/db"
15+
user_model "code.gitea.io/gitea/models/user"
16+
"code.gitea.io/gitea/modules/storage"
17+
"code.gitea.io/gitea/modules/util"
18+
)
19+
20+
// DeleteUser completely and permanently deletes everything of a user,
21+
// but issues/comments/pulls will be kept and shown as someone has been deleted,
22+
// unless the user is younger than USER_DELETE_WITH_COMMENTS_MAX_DAYS.
23+
func DeleteUser(u *models.User) error {
24+
if u.IsOrganization() {
25+
return fmt.Errorf("%s is an organization not a user", u.Name)
26+
}
27+
28+
ctx, commiter, err := db.TxContext()
29+
if err != nil {
30+
return err
31+
}
32+
defer commiter.Close()
33+
34+
// Note: A user owns any repository or belongs to any organization
35+
// cannot perform delete operation.
36+
37+
// Check ownership of repository.
38+
count, err := models.GetRepositoryCount(ctx, u)
39+
if err != nil {
40+
return fmt.Errorf("GetRepositoryCount: %v", err)
41+
} else if count > 0 {
42+
return models.ErrUserOwnRepos{UID: u.ID}
43+
}
44+
45+
// Check membership of organization.
46+
count, err = models.GetOrganizationCount(ctx, u)
47+
if err != nil {
48+
return fmt.Errorf("GetOrganizationCount: %v", err)
49+
} else if count > 0 {
50+
return models.ErrUserHasOrgs{UID: u.ID}
51+
}
52+
53+
if err := models.DeleteUser(ctx, u); err != nil {
54+
return fmt.Errorf("DeleteUser: %v", err)
55+
}
56+
57+
if err := commiter.Commit(); err != nil {
58+
return err
59+
}
60+
61+
// Note: There are something just cannot be roll back,
62+
// so just keep error logs of those operations.
63+
path := models.UserPath(u.Name)
64+
if err := util.RemoveAll(path); err != nil {
65+
err = fmt.Errorf("Failed to RemoveAll %s: %v", path, err)
66+
_ = admin_model.CreateNotice(db.DefaultContext, admin_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
67+
return err
68+
}
69+
70+
if u.Avatar != "" {
71+
avatarPath := u.CustomAvatarRelativePath()
72+
if err := storage.Avatars.Delete(avatarPath); err != nil {
73+
err = fmt.Errorf("Failed to remove %s: %v", avatarPath, err)
74+
_ = admin_model.CreateNotice(db.DefaultContext, admin_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
75+
return err
76+
}
77+
}
78+
79+
return nil
80+
}
81+
82+
// DeleteInactiveUsers deletes all inactive users and email addresses.
83+
func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
84+
users, err := models.GetInactiveUsers(ctx, olderThan)
85+
if err != nil {
86+
return err
87+
}
88+
89+
// FIXME: should only update authorized_keys file once after all deletions.
90+
for _, u := range users {
91+
select {
92+
case <-ctx.Done():
93+
return db.ErrCancelledf("Before delete inactive user %s", u.Name)
94+
default:
95+
}
96+
if err := DeleteUser(u); err != nil {
97+
// Ignore users that were set inactive by admin.
98+
if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) {
99+
continue
100+
}
101+
return err
102+
}
103+
}
104+
105+
return user_model.DeleteInactiveEmailAddresses(ctx)
106+
}

‎services/user/user_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package user
6+
7+
import (
8+
"path/filepath"
9+
"testing"
10+
11+
"code.gitea.io/gitea/models"
12+
"code.gitea.io/gitea/models/db"
13+
"code.gitea.io/gitea/models/unittest"
14+
"code.gitea.io/gitea/modules/setting"
15+
16+
"github.com/stretchr/testify/assert"
17+
)
18+
19+
func TestMain(m *testing.M) {
20+
unittest.MainTest(m, filepath.Join("..", ".."))
21+
}
22+
23+
func TestDeleteUser(t *testing.T) {
24+
test := func(userID int64) {
25+
assert.NoError(t, unittest.PrepareTestDatabase())
26+
user := unittest.AssertExistsAndLoadBean(t, &models.User{ID: userID}).(*models.User)
27+
28+
ownedRepos := make([]*models.Repository, 0, 10)
29+
assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&ownedRepos, &models.Repository{OwnerID: userID}))
30+
if len(ownedRepos) > 0 {
31+
err := DeleteUser(user)
32+
assert.Error(t, err)
33+
assert.True(t, models.IsErrUserOwnRepos(err))
34+
return
35+
}
36+
37+
orgUsers := make([]*models.OrgUser, 0, 10)
38+
assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &models.OrgUser{UID: userID}))
39+
for _, orgUser := range orgUsers {
40+
if err := models.RemoveOrgUser(orgUser.OrgID, orgUser.UID); err != nil {
41+
assert.True(t, models.IsErrLastOrgOwner(err))
42+
return
43+
}
44+
}
45+
assert.NoError(t, DeleteUser(user))
46+
unittest.AssertNotExistsBean(t, &models.User{ID: userID})
47+
unittest.CheckConsistencyFor(t, &models.User{}, &models.Repository{})
48+
}
49+
test(2)
50+
test(4)
51+
test(8)
52+
test(11)
53+
54+
org := unittest.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User)
55+
assert.Error(t, DeleteUser(org))
56+
}
57+
58+
func TestCreateUser(t *testing.T) {
59+
user := &models.User{
60+
Name: "GiteaBot",
61+
Email: "GiteaBot@gitea.io",
62+
Passwd: ";p['////..-++']",
63+
IsAdmin: false,
64+
Theme: setting.UI.DefaultTheme,
65+
MustChangePassword: false,
66+
}
67+
68+
assert.NoError(t, models.CreateUser(user))
69+
70+
assert.NoError(t, DeleteUser(user))
71+
}
72+
73+
func TestCreateUser_Issue5882(t *testing.T) {
74+
// Init settings
75+
_ = setting.Admin
76+
77+
passwd := ".//.;1;;//.,-=_"
78+
79+
tt := []struct {
80+
user *models.User
81+
disableOrgCreation bool
82+
}{
83+
{&models.User{Name: "GiteaBot", Email: "GiteaBot@gitea.io", Passwd: passwd, MustChangePassword: false}, false},
84+
{&models.User{Name: "GiteaBot2", Email: "GiteaBot2@gitea.io", Passwd: passwd, MustChangePassword: false}, true},
85+
}
86+
87+
setting.Service.DefaultAllowCreateOrganization = true
88+
89+
for _, v := range tt {
90+
setting.Admin.DisableRegularOrgCreation = v.disableOrgCreation
91+
92+
assert.NoError(t, models.CreateUser(v.user))
93+
94+
u, err := models.GetUserByEmail(v.user.Email)
95+
assert.NoError(t, err)
96+
97+
assert.Equal(t, !u.AllowCreateOrganization, v.disableOrgCreation)
98+
99+
assert.NoError(t, DeleteUser(v.user))
100+
}
101+
}

‎services/wiki/wiki.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"code.gitea.io/gitea/models"
1515
admin_model "code.gitea.io/gitea/models/admin"
16+
"code.gitea.io/gitea/models/db"
1617
"code.gitea.io/gitea/models/unit"
1718
"code.gitea.io/gitea/modules/git"
1819
"code.gitea.io/gitea/modules/log"
@@ -375,6 +376,6 @@ func DeleteWiki(repo *models.Repository) error {
375376
return err
376377
}
377378

378-
admin_model.RemoveAllWithNotice("Delete repository wiki", repo.WikiPath())
379+
admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository wiki", repo.WikiPath())
379380
return nil
380381
}

0 commit comments

Comments
 (0)
Please sign in to comment.