From a93f13849cd7f54029f1dc17b642d024b98ee71e Mon Sep 17 00:00:00 2001 From: David Schneiderbauer Date: Tue, 19 Jun 2018 21:44:33 +0200 Subject: [PATCH] Fix not removed watches on unallowed repositories (#4201) --- models/issue_watch.go | 12 +++ models/migrations/migrations.go | 2 + models/migrations/v67.go | 158 ++++++++++++++++++++++++++++++++ models/org_team.go | 49 ++++++++++ models/repo.go | 3 + models/repo_collaboration.go | 9 ++ 6 files changed, 233 insertions(+) create mode 100644 models/migrations/v67.go diff --git a/models/issue_watch.go b/models/issue_watch.go index 69e218af01..3e7d24821b 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -71,3 +71,15 @@ func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error Find(&watches) return } + +func removeIssueWatchersByRepoID(e Engine, userID int64, repoID int64) error { + iw := &IssueWatch{ + IsWatching: false, + } + _, err := e. + Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", repoID). + Cols("is_watching", "updated_unix"). + Where("`issue_watch`.user_id = ?", userID). + Update(iw) + return err +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 1300065ab4..2537e5712b 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -186,6 +186,8 @@ var migrations = []Migration{ NewMigration("add u2f", addU2FReg), // v66 -> v67 NewMigration("add login source id column for public_key table", addLoginSourceIDToPublicKeyTable), + // v67 -> v68 + NewMigration("remove stale watches", removeStaleWatches), } // Migrate database to current version diff --git a/models/migrations/v67.go b/models/migrations/v67.go new file mode 100644 index 0000000000..2782219191 --- /dev/null +++ b/models/migrations/v67.go @@ -0,0 +1,158 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "code.gitea.io/gitea/modules/setting" + + "github.com/go-xorm/xorm" +) + +func removeStaleWatches(x *xorm.Engine) error { + type Watch struct { + ID int64 + UserID int64 + RepoID int64 + } + + type IssueWatch struct { + ID int64 + UserID int64 + RepoID int64 + IsWatching bool + } + + type Repository struct { + ID int64 + IsPrivate bool + OwnerID int64 + } + + type Access struct { + UserID int64 + RepoID int64 + Mode int + } + + const ( + // AccessModeNone no access + AccessModeNone int = iota // 0 + // AccessModeRead read access + AccessModeRead // 1 + ) + + accessLevel := func(userID int64, repo *Repository) (int, error) { + mode := AccessModeNone + if !repo.IsPrivate { + mode = AccessModeRead + } + + if userID == 0 { + return mode, nil + } + + if userID == repo.OwnerID { + return 4, nil + } + + a := &Access{UserID: userID, RepoID: repo.ID} + if has, err := x.Get(a); !has || err != nil { + return mode, err + } + return a.Mode, nil + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + repoCache := make(map[int64]*Repository) + err := x.BufferSize(setting.IterateBufferSize).Iterate(new(Watch), + func(idx int, bean interface{}) error { + watch := bean.(*Watch) + + repo := repoCache[watch.RepoID] + if repo == nil { + repo = &Repository{ + ID: watch.RepoID, + } + if _, err := x.Get(repo); err != nil { + return err + } + repoCache[watch.RepoID] = repo + } + + // Remove watches from now unaccessible repositories + mode, err := accessLevel(watch.UserID, repo) + if err != nil { + return err + } + has := AccessModeRead <= mode + if has { + return nil + } + + if _, err = sess.Delete(&Watch{0, watch.UserID, repo.ID}); err != nil { + return err + } + _, err = sess.Exec("UPDATE `repository` SET num_watches = num_watches - 1 WHERE id = ?", repo.ID) + + return err + }) + if err != nil { + return err + } + + repoCache = make(map[int64]*Repository) + err = x.BufferSize(setting.IterateBufferSize). + Distinct("issue_watch.user_id", "issue.repo_id"). + Join("INNER", "issue", "issue_watch.issue_id = issue.id"). + Where("issue_watch.is_watching = ?", true). + Iterate(new(IssueWatch), + func(idx int, bean interface{}) error { + watch := bean.(*IssueWatch) + + repo := repoCache[watch.RepoID] + if repo == nil { + repo = &Repository{ + ID: watch.RepoID, + } + if _, err := x.Get(repo); err != nil { + return err + } + repoCache[watch.RepoID] = repo + } + + // Remove issue watches from now unaccssible repositories + mode, err := accessLevel(watch.UserID, repo) + if err != nil { + return err + } + has := AccessModeRead <= mode + if has { + return nil + } + + iw := &IssueWatch{ + IsWatching: false, + } + + _, err = sess. + Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", watch.RepoID). + Cols("is_watching", "updated_unix"). + Where("`issue_watch`.user_id = ?", watch.UserID). + Update(iw) + + return err + + }) + if err != nil { + return err + } + + return sess.Commit() +} diff --git a/models/org_team.go b/models/org_team.go index 9d8a031418..5ea6e76cd9 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -178,6 +178,11 @@ func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (e if err = watchRepo(e, teamUser.UID, repo.ID, false); err != nil { return err } + + // Remove all IssueWatches a user has subscribed to in the repositories + if err := removeIssueWatchersByRepoID(e, teamUser.UID, repo.ID); err != nil { + return err + } } return nil @@ -374,11 +379,34 @@ func DeleteTeam(t *Team) error { return err } + if err := t.getMembers(sess); err != nil { + return err + } + // Delete all accesses. for _, repo := range t.Repos { if err := repo.recalculateTeamAccesses(sess, t.ID); err != nil { return err } + + // Remove watches from all users and now unaccessible repos + for _, user := range t.Members { + has, err := hasAccess(sess, user.ID, repo, AccessModeRead) + if err != nil { + return err + } else if has { + continue + } + + if err = watchRepo(sess, user.ID, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repositories + if err = removeIssueWatchersByRepoID(sess, user.ID, repo.ID); err != nil { + return err + } + } } // Delete team-repo @@ -518,6 +546,10 @@ func AddTeamMember(team *Team, userID int64) error { if err := repo.recalculateTeamAccesses(sess, 0); err != nil { return err } + + if err = watchRepo(sess, userID, repo.ID, true); err != nil { + return err + } } return sess.Commit() @@ -558,6 +590,23 @@ func removeTeamMember(e *xorm.Session, team *Team, userID int64) error { if err := repo.recalculateTeamAccesses(e, 0); err != nil { return err } + + // Remove watches from now unaccessible + has, err := hasAccess(e, userID, repo, AccessModeRead) + if err != nil { + return err + } else if has { + continue + } + + if err = watchRepo(e, userID, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repositories + if err := removeIssueWatchersByRepoID(e, userID, repo.ID); err != nil { + return err + } } // Check if the user is a member of any team in the organization. diff --git a/models/repo.go b/models/repo.go index f4923cf4a9..7f2be502a4 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1851,6 +1851,9 @@ func DeleteRepository(doer *User, uid, repoID int64) error { if _, err = sess.In("issue_id", issueIDs).Delete(&Reaction{}); err != nil { return err } + if _, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{}); err != nil { + return err + } attachments := make([]*Attachment, 0, 5) if err = sess. diff --git a/models/repo_collaboration.go b/models/repo_collaboration.go index 0448149e6a..9d2935d581 100644 --- a/models/repo_collaboration.go +++ b/models/repo_collaboration.go @@ -172,5 +172,14 @@ func (repo *Repository) DeleteCollaboration(uid int64) (err error) { return err } + if err = watchRepo(sess, uid, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repository + if err := removeIssueWatchersByRepoID(sess, uid, repo.ID); err != nil { + return err + } + return sess.Commit() }