Nicely handle missing user in collaborations (#17049)

* Nicely handle missing user in collaborations

It is possible to have a collaboration in a repository which refers to a no-longer
existing user. This causes the repository transfer to fail with an unusual error.

This PR makes `repo.getCollaborators()` nicely handle the missing user by ghosting
the collaboration but also adds consistency check. It also adds an
Access consistency check.

Fix #17044

Signed-off-by: Andrew Thornton <art27@cantab.net>

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
tokarchuk/v1.17
zeripath 3 years ago committed by GitHub
parent b5856c4437
commit e8574f2f7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      models/access.go
  2. 14
      models/repo_collaboration.go
  3. 9
      models/repo_transfer.go
  4. 371
      modules/doctor/dbconsistency.go

@ -230,6 +230,9 @@ func (repo *Repository) refreshCollaboratorAccesses(e db.Engine, accessMap map[i
return fmt.Errorf("getCollaborations: %v", err) return fmt.Errorf("getCollaborations: %v", err)
} }
for _, c := range collaborators { for _, c := range collaborators {
if c.User.IsGhost() {
continue
}
updateUserAccess(accessMap, c.User, c.Collaboration.Mode) updateUserAccess(accessMap, c.User, c.Collaboration.Mode)
} }
return nil return nil

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder" "xorm.io/builder"
@ -88,16 +89,21 @@ func (repo *Repository) getCollaborators(e db.Engine, listOptions db.ListOptions
return nil, fmt.Errorf("getCollaborations: %v", err) return nil, fmt.Errorf("getCollaborations: %v", err)
} }
collaborators := make([]*Collaborator, len(collaborations)) collaborators := make([]*Collaborator, 0, len(collaborations))
for i, c := range collaborations { for _, c := range collaborations {
user, err := getUserByID(e, c.UserID) user, err := getUserByID(e, c.UserID)
if err != nil { if err != nil {
if IsErrUserNotExist(err) {
log.Warn("Inconsistent DB: User: %d is listed as collaborator of %-v but does not exist", c.UserID, repo)
user = NewGhostUser()
} else {
return nil, err return nil, err
} }
collaborators[i] = &Collaborator{ }
collaborators = append(collaborators, &Collaborator{
User: user, User: user,
Collaboration: c, Collaboration: c,
} })
} }
return collaborators, nil return collaborators, nil
} }

@ -274,6 +274,14 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) (err e
// Dummy object. // Dummy object.
collaboration := &Collaboration{RepoID: repo.ID} collaboration := &Collaboration{RepoID: repo.ID}
for _, c := range collaborators { for _, c := range collaborators {
if c.IsGhost() {
collaboration.ID = c.Collaboration.ID
if _, err := sess.Delete(collaboration); err != nil {
return fmt.Errorf("remove collaborator '%d': %v", c.ID, err)
}
collaboration.ID = 0
}
if c.ID != newOwner.ID { if c.ID != newOwner.ID {
isMember, err := isOrganizationMember(sess, newOwner.ID, c.ID) isMember, err := isOrganizationMember(sess, newOwner.ID, c.ID)
if err != nil { if err != nil {
@ -286,6 +294,7 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) (err e
if _, err := sess.Delete(collaboration); err != nil { if _, err := sess.Delete(collaboration); err != nil {
return fmt.Errorf("remove collaborator '%d': %v", c.ID, err) return fmt.Errorf("remove collaborator '%d': %v", c.ID, err)
} }
collaboration.UserID = 0
} }
// Remove old team-repository relations. // Remove old team-repository relations.

@ -14,289 +14,174 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
) )
func checkDBConsistency(logger log.Logger, autofix bool) error { type consistencyCheck struct {
// make sure DB version is uptodate Name string
if err := db.NewEngine(context.Background(), migrations.EnsureUpToDate); err != nil { Counter func() (int64, error)
logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded") Fixer func() (int64, error)
return err FixedMessage string
} }
// find labels without existing repo or org func (c *consistencyCheck) Run(logger log.Logger, autofix bool) error {
count, err := models.CountOrphanedLabels() count, err := c.Counter()
if err != nil { if err != nil {
logger.Critical("Error: %v whilst counting orphaned labels", err) logger.Critical("Error: %v whilst counting %s", err, c.Name)
return err return err
} }
if count > 0 { if count > 0 {
if autofix { if autofix {
if err = models.DeleteOrphanedLabels(); err != nil { var fixed int64
logger.Critical("Error: %v whilst deleting orphaned labels", err) if fixed, err = c.Fixer(); err != nil {
logger.Critical("Error: %v whilst fixing %s", err, c.Name)
return err return err
} }
logger.Info("%d labels without existing repository/organisation deleted", count)
} else {
logger.Warn("%d labels without existing repository/organisation", count)
}
}
// find IssueLabels without existing label prompt := "Deleted"
count, err = models.CountOrphanedIssueLabels() if c.FixedMessage != "" {
if err != nil { prompt = c.FixedMessage
logger.Critical("Error: %v whilst counting orphaned issue_labels", err)
return err
}
if count > 0 {
if autofix {
if err = models.DeleteOrphanedIssueLabels(); err != nil {
logger.Critical("Error: %v whilst deleting orphaned issue_labels", err)
return err
}
logger.Info("%d issue_labels without existing label deleted", count)
} else {
logger.Warn("%d issue_labels without existing label", count)
}
} }
// find issues without existing repository if fixed < 0 {
count, err = models.CountOrphanedIssues() logger.Info(prompt+" %d %s", count, c.Name)
if err != nil {
logger.Critical("Error: %v whilst counting orphaned issues", err)
return err
}
if count > 0 {
if autofix {
if err = models.DeleteOrphanedIssues(); err != nil {
logger.Critical("Error: %v whilst deleting orphaned issues", err)
return err
}
logger.Info("%d issues without existing repository deleted", count)
} else { } else {
logger.Warn("%d issues without existing repository", count) logger.Info(prompt+" %d/%d %s", fixed, count, c.Name)
}
} }
// find releases without existing repository
count, err = models.CountOrphanedObjects("release", "repository", "release.repo_id=repository.id")
if err != nil {
logger.Critical("Error: %v whilst counting orphaned objects", err)
return err
}
if count > 0 {
if autofix {
if err = models.DeleteOrphanedObjects("release", "repository", "release.repo_id=repository.id"); err != nil {
logger.Critical("Error: %v whilst deleting orphaned objects", err)
return err
}
logger.Info("%d releases without existing repository deleted", count)
} else { } else {
logger.Warn("%d releases without existing repository", count) logger.Warn("Found %d %s", count, c.Name)
} }
} }
return nil
}
// find pulls without existing issues func asFixer(fn func() error) func() (int64, error) {
count, err = models.CountOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id") return func() (int64, error) {
if err != nil { err := fn()
logger.Critical("Error: %v whilst counting orphaned objects", err) return -1, err
return err
}
if count > 0 {
if autofix {
if err = models.DeleteOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id"); err != nil {
logger.Critical("Error: %v whilst deleting orphaned objects", err)
return err
}
logger.Info("%d pull requests without existing issue deleted", count)
} else {
logger.Warn("%d pull requests without existing issue", count)
}
} }
}
// find tracked times without existing issues/pulls func genericOrphanCheck(name, subject, refobject, joincond string) consistencyCheck {
count, err = models.CountOrphanedObjects("tracked_time", "issue", "tracked_time.issue_id=issue.id") return consistencyCheck{
if err != nil { Name: name,
logger.Critical("Error: %v whilst counting orphaned objects", err) Counter: func() (int64, error) {
return err return models.CountOrphanedObjects(subject, refobject, joincond)
} },
if count > 0 { Fixer: func() (int64, error) {
if autofix { err := models.DeleteOrphanedObjects(subject, refobject, joincond)
if err = models.DeleteOrphanedObjects("tracked_time", "issue", "tracked_time.issue_id=issue.id"); err != nil { return -1, err
logger.Critical("Error: %v whilst deleting orphaned objects", err) },
return err
}
logger.Info("%d tracked times without existing issue deleted", count)
} else {
logger.Warn("%d tracked times without existing issue", count)
}
} }
}
// find attachments without existing issues or releases func checkDBConsistency(logger log.Logger, autofix bool) error {
count, err = models.CountOrphanedAttachments() // make sure DB version is uptodate
if err != nil { if err := db.NewEngine(context.Background(), migrations.EnsureUpToDate); err != nil {
logger.Critical("Error: %v whilst counting orphaned objects", err) logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
return err
}
if count > 0 {
if autofix {
if err = models.DeleteOrphanedAttachments(); err != nil {
logger.Critical("Error: %v whilst deleting orphaned objects", err)
return err return err
} }
logger.Info("%d attachments without existing issue or release deleted", count)
} else {
logger.Warn("%d attachments without existing issue or release", count)
}
}
consistencyChecks := []consistencyCheck{
{
// find labels without existing repo or org
Name: "Orphaned Labels without existing repository or organisation",
Counter: models.CountOrphanedLabels,
Fixer: asFixer(models.DeleteOrphanedLabels),
},
{
// find IssueLabels without existing label
Name: "Orphaned Issue Labels without existing label",
Counter: models.CountOrphanedIssueLabels,
Fixer: asFixer(models.DeleteOrphanedIssueLabels),
},
{
// find issues without existing repository
Name: "Orphaned Issues without existing repository",
Counter: models.CountOrphanedIssues,
Fixer: asFixer(models.DeleteOrphanedIssues),
},
// find releases without existing repository
genericOrphanCheck("Orphaned Releases without existing repository",
"release", "repository", "release.repo_id=repository.id"),
// find pulls without existing issues
genericOrphanCheck("Orphaned PullRequests without existing issue",
"pull_request", "issue", "pull_request.issue_id=issue.id"),
// find tracked times without existing issues/pulls
genericOrphanCheck("Orphaned TrackedTimes without existing issue",
"tracked_time", "issue", "tracked_time.issue_id=issue.id"),
// find attachments without existing issues or releases
{
Name: "Orphaned Attachments without existing issues or releases",
Counter: models.CountOrphanedAttachments,
Fixer: asFixer(models.DeleteOrphanedAttachments),
},
// find null archived repositories // find null archived repositories
count, err = models.CountNullArchivedRepository() {
if err != nil { Name: "Repositories with is_archived IS NULL",
logger.Critical("Error: %v whilst counting null archived repositories", err) Counter: models.CountNullArchivedRepository,
return err Fixer: models.FixNullArchivedRepository,
} FixedMessage: "Fixed",
if count > 0 { },
if autofix {
updatedCount, err := models.FixNullArchivedRepository()
if err != nil {
logger.Critical("Error: %v whilst fixing null archived repositories", err)
return err
}
logger.Info("%d repositories with null is_archived updated", updatedCount)
} else {
logger.Warn("%d repositories with null is_archived", count)
}
}
// find label comments with empty labels // find label comments with empty labels
count, err = models.CountCommentTypeLabelWithEmptyLabel() {
if err != nil { Name: "Label comments with empty labels",
logger.Critical("Error: %v whilst counting label comments with empty labels", err) Counter: models.CountCommentTypeLabelWithEmptyLabel,
return err Fixer: models.FixCommentTypeLabelWithEmptyLabel,
} FixedMessage: "Fixed",
if count > 0 { },
if autofix {
updatedCount, err := models.FixCommentTypeLabelWithEmptyLabel()
if err != nil {
logger.Critical("Error: %v whilst removing label comments with empty labels", err)
return err
}
logger.Info("%d label comments with empty labels removed", updatedCount)
} else {
logger.Warn("%d label comments with empty labels", count)
}
}
// find label comments with labels from outside the repository // find label comments with labels from outside the repository
count, err = models.CountCommentTypeLabelWithOutsideLabels() {
if err != nil { Name: "Label comments with labels from outside the repository",
logger.Critical("Error: %v whilst counting label comments with outside labels", err) Counter: models.CountCommentTypeLabelWithOutsideLabels,
return err Fixer: models.FixCommentTypeLabelWithOutsideLabels,
} FixedMessage: "Removed",
if count > 0 { },
if autofix {
updatedCount, err := models.FixCommentTypeLabelWithOutsideLabels()
if err != nil {
logger.Critical("Error: %v whilst removing label comments with outside labels", err)
return err
}
log.Info("%d label comments with outside labels removed", updatedCount)
} else {
log.Warn("%d label comments with outside labels", count)
}
}
// find issue_label with labels from outside the repository // find issue_label with labels from outside the repository
count, err = models.CountIssueLabelWithOutsideLabels() {
if err != nil { Name: "IssueLabels with Labels from outside the repository",
logger.Critical("Error: %v whilst counting issue_labels from outside the repository or organisation", err) Counter: models.CountIssueLabelWithOutsideLabels,
return err Fixer: models.FixIssueLabelWithOutsideLabels,
} FixedMessage: "Removed",
if count > 0 { },
if autofix {
updatedCount, err := models.FixIssueLabelWithOutsideLabels()
if err != nil {
logger.Critical("Error: %v whilst removing issue_labels from outside the repository or organisation", err)
return err
}
logger.Info("%d issue_labels from outside the repository or organisation removed", updatedCount)
} else {
logger.Warn("%d issue_labels from outside the repository or organisation", count)
}
} }
// TODO: function to recalc all counters // TODO: function to recalc all counters
if setting.Database.UsePostgreSQL { if setting.Database.UsePostgreSQL {
count, err = db.CountBadSequences() consistencyChecks = append(consistencyChecks, consistencyCheck{
if err != nil { Name: "Sequence values",
logger.Critical("Error: %v whilst checking sequence values", err) Counter: db.CountBadSequences,
return err Fixer: asFixer(db.FixBadSequences),
} FixedMessage: "Updated",
if count > 0 { })
if autofix {
err := db.FixBadSequences()
if err != nil {
logger.Critical("Error: %v whilst attempting to fix sequences", err)
return err
}
logger.Info("%d sequences updated", count)
} else {
logger.Warn("%d sequences with incorrect values", count)
}
}
} }
consistencyChecks = append(consistencyChecks,
// find protected branches without existing repository // find protected branches without existing repository
count, err = models.CountOrphanedObjects("protected_branch", "repository", "protected_branch.repo_id=repository.id") genericOrphanCheck("Protected Branches without existing repository",
if err != nil { "protected_branch", "repository", "protected_branch.repo_id=repository.id"),
logger.Critical("Error: %v whilst counting orphaned objects", err)
return err
}
if count > 0 {
if autofix {
if err = models.DeleteOrphanedObjects("protected_branch", "repository", "protected_branch.repo_id=repository.id"); err != nil {
logger.Critical("Error: %v whilst deleting orphaned objects", err)
return err
}
logger.Info("%d protected branches without existing repository deleted", count)
} else {
logger.Warn("%d protected branches without existing repository", count)
}
}
// find deleted branches without existing repository // find deleted branches without existing repository
count, err = models.CountOrphanedObjects("deleted_branch", "repository", "deleted_branch.repo_id=repository.id") genericOrphanCheck("Deleted Branches without existing repository",
if err != nil { "deleted_branch", "repository", "deleted_branch.repo_id=repository.id"),
logger.Critical("Error: %v whilst counting orphaned objects", err)
return err
}
if count > 0 {
if autofix {
if err = models.DeleteOrphanedObjects("deleted_branch", "repository", "deleted_branch.repo_id=repository.id"); err != nil {
logger.Critical("Error: %v whilst deleting orphaned objects", err)
return err
}
logger.Info("%d deleted branches without existing repository deleted", count)
} else {
logger.Warn("%d deleted branches without existing repository", count)
}
}
// find LFS locks without existing repository // find LFS locks without existing repository
count, err = models.CountOrphanedObjects("lfs_lock", "repository", "lfs_lock.repo_id=repository.id") genericOrphanCheck("LFS locks without existing repository",
if err != nil { "lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),
logger.Critical("Error: %v whilst counting orphaned objects", err) // find collaborations without users
return err genericOrphanCheck("Collaborations without existing user",
} "collaboration", "user", "collaboration.user_id=user.id"),
if count > 0 { // find collaborations without repository
if autofix { genericOrphanCheck("Collaborations without existing repository",
if err = models.DeleteOrphanedObjects("lfs_lock", "repository", "lfs_lock.repo_id=repository.id"); err != nil { "collaboration", "repository", "collaboration.repo_id=repository.id"),
logger.Critical("Error: %v whilst deleting orphaned objects", err) // find access without users
genericOrphanCheck("Access entries without existing user",
"access", "user", "access.user_id=user.id"),
// find access without repository
genericOrphanCheck("Access entries without existing repository",
"access", "repository", "access.repo_id=repository.id"),
)
for _, c := range consistencyChecks {
if err := c.Run(logger, autofix); err != nil {
return err return err
} }
logger.Info("%d LFS locks without existing repository deleted", count)
} else {
logger.Warn("%d LFS locks without existing repository", count)
}
} }
return nil return nil

Loading…
Cancel
Save