Enhance Ghost comment mitigation Settings (#14392)

* refactor models.DeleteComment and delete related reactions too

* use deleteComment for UserDeleteWithCommentsMaxDays in DeleteUser

* nits

* Use time.Duration as other time settings have

* docs

* Resolve Fixme & fix potential deadlock

* Disabled by Default

* Update Config Value Description

* switch args

* Update models/issue_comment.go

Co-authored-by: zeripath <art27@cantab.net>

Co-authored-by: zeripath <art27@cantab.net>
This commit is contained in:
6543 2021-01-22 03:56:19 +01:00 committed by GitHub
parent 0e2e73410e
commit a0e424da85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 72 additions and 41 deletions

View file

@ -688,9 +688,8 @@ AUTO_WATCH_NEW_REPOS = true
; Default value for AutoWatchOnChanges ; Default value for AutoWatchOnChanges
; Make the user watch a repository When they commit for the first time ; Make the user watch a repository When they commit for the first time
AUTO_WATCH_ON_CHANGES = false AUTO_WATCH_ON_CHANGES = false
; Default value for the minimum age a user has to exist before deletion to keep issue comments. ; Minimum amount of time a user must exist before comments are kept when the user is deleted.
; If a user deletes his account before that amount of days, his comments will be deleted as well. USER_DELETE_WITH_COMMENTS_MAX_TIME = 0
USER_DELETE_WITH_COMMENTS_MAX_DAYS = 0
[webhook] [webhook]
; Hook task queue length, increase if webhook shooting starts hanging ; Hook task queue length, increase if webhook shooting starts hanging

View file

@ -474,7 +474,7 @@ relation to port exhaustion.
- `ALLOW_ONLY_EXTERNAL_REGISTRATION`: **false** Set to true to force registration only using third-party services. - `ALLOW_ONLY_EXTERNAL_REGISTRATION`: **false** Set to true to force registration only using third-party services.
- `NO_REPLY_ADDRESS`: **DOMAIN** Default value for the domain part of the user's email address in the git log if he has set KeepEmailPrivate to true. - `NO_REPLY_ADDRESS`: **DOMAIN** Default value for the domain part of the user's email address in the git log if he has set KeepEmailPrivate to true.
The user's email will be replaced with a concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. The user's email will be replaced with a concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS.
- `USER_DELETE_WITH_COMMENTS_MAX_DAYS`: **0** If a user deletes his account before that amount of days, his comments will be deleted as well. - `USER_DELETE_WITH_COMMENTS_MAX_TIME`: **0** Minimum amount of time a user must exist before comments are kept when the user is deleted.
## SSH Minimum Key Sizes (`ssh.minimum_key_sizes`) ## SSH Minimum Key Sizes (`ssh.minimum_key_sizes`)

View file

@ -75,7 +75,7 @@ func removeStorageWithNotice(e Engine, bucket storage.ObjectStorage, title, path
if err := bucket.Delete(path); err != nil { if err := bucket.Delete(path); err != nil {
desc := fmt.Sprintf("%s [%s]: %v", title, path, err) desc := fmt.Sprintf("%s [%s]: %v", title, path, err)
log.Warn(title+" [%s]: %v", path, err) log.Warn(title+" [%s]: %v", path, err)
if err = createNotice(x, NoticeRepository, desc); err != nil { if err = createNotice(e, NoticeRepository, desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err) log.Error("CreateRepositoryNotice: %v", err)
} }
} }

View file

@ -82,7 +82,7 @@ func isUserAssignedToIssue(e Engine, issue *Issue, user *User) (isAssigned bool,
} }
// ClearAssigneeByUserID deletes all assignments of an user // ClearAssigneeByUserID deletes all assignments of an user
func clearAssigneeByUserID(sess *xorm.Session, userID int64) (err error) { func clearAssigneeByUserID(sess Engine, userID int64) (err error) {
_, err = sess.Delete(&IssueAssignees{AssigneeID: userID}) _, err = sess.Delete(&IssueAssignees{AssigneeID: userID})
return return
} }

View file

@ -1037,33 +1037,41 @@ func UpdateComment(c *Comment, doer *User) error {
} }
// DeleteComment deletes the comment // DeleteComment deletes the comment
func DeleteComment(comment *Comment, doer *User) error { func DeleteComment(comment *Comment) error {
sess := x.NewSession() sess := x.NewSession()
defer sess.Close() defer sess.Close()
if err := sess.Begin(); err != nil { if err := sess.Begin(); err != nil {
return err return err
} }
if _, err := sess.Delete(&Comment{ if err := deleteComment(sess, comment); err != nil {
return err
}
return sess.Commit()
}
func deleteComment(e Engine, comment *Comment) error {
if _, err := e.Delete(&Comment{
ID: comment.ID, ID: comment.ID,
}); err != nil { }); err != nil {
return err return err
} }
if comment.Type == CommentTypeComment { if comment.Type == CommentTypeComment {
if _, err := sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil { if _, err := e.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
return err return err
} }
} }
if _, err := sess.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil { if _, err := e.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil {
return err return err
} }
if err := comment.neuterCrossReferences(sess); err != nil { if err := comment.neuterCrossReferences(e); err != nil {
return err return err
} }
return sess.Commit() return deleteReaction(e, &ReactionOptions{Comment: comment})
} }
// CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS

View file

@ -178,11 +178,15 @@ func CreateCommentReaction(doer *User, issue *Issue, comment *Comment, content s
}) })
} }
func deleteReaction(e *xorm.Session, opts *ReactionOptions) error { func deleteReaction(e Engine, opts *ReactionOptions) error {
reaction := &Reaction{ reaction := &Reaction{
Type: opts.Type, Type: opts.Type,
UserID: opts.Doer.ID, }
IssueID: opts.Issue.ID, if opts.Doer != nil {
reaction.UserID = opts.Doer.ID
}
if opts.Issue != nil {
reaction.IssueID = opts.Issue.ID
} }
if opts.Comment != nil { if opts.Comment != nil {
reaction.CommentID = opts.Comment.ID reaction.CommentID = opts.Comment.ID

View file

@ -38,7 +38,6 @@ import (
"golang.org/x/crypto/scrypt" "golang.org/x/crypto/scrypt"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"xorm.io/builder" "xorm.io/builder"
"xorm.io/xorm"
) )
// UserType defines the user type // UserType defines the user type
@ -1071,8 +1070,7 @@ func deleteBeans(e Engine, beans ...interface{}) (err error) {
return nil return nil
} }
// FIXME: need some kind of mechanism to record failure. HINT: system notice func deleteUser(e Engine, u *User) error {
func deleteUser(e *xorm.Session, u *User) error {
// Note: A user owns any repository or belongs to any organization // Note: A user owns any repository or belongs to any organization
// cannot perform delete operation. // cannot perform delete operation.
@ -1151,12 +1149,30 @@ func deleteUser(e *xorm.Session, u *User) error {
return fmt.Errorf("deleteBeans: %v", err) return fmt.Errorf("deleteBeans: %v", err)
} }
if setting.Service.UserDeleteWithCommentsMaxDays != 0 && if setting.Service.UserDeleteWithCommentsMaxTime != 0 &&
u.CreatedUnix.AsTime().Add(time.Duration(setting.Service.UserDeleteWithCommentsMaxDays)*24*time.Hour).After(time.Now()) { u.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now()) {
if err = deleteBeans(e,
&Comment{PosterID: u.ID}, // Delete Comments
); err != nil { const batchSize = 50
return fmt.Errorf("deleteBeans: %v", err) for start := 0; ; start += batchSize {
comments := make([]*Comment, 0, batchSize)
if err = e.Where("type=? AND poster_id=?", CommentTypeComment, u.ID).Limit(batchSize, start).Find(&comments); err != nil {
return err
}
if len(comments) == 0 {
break
}
for _, comment := range comments {
if err = deleteComment(e, comment); err != nil {
return err
}
}
}
// Delete Reactions
if err = deleteReaction(e, &ReactionOptions{Doer: u}); err != nil {
return err
} }
} }
@ -1195,18 +1211,21 @@ func deleteUser(e *xorm.Session, u *User) error {
return fmt.Errorf("Delete: %v", err) return fmt.Errorf("Delete: %v", err)
} }
// FIXME: system notice
// Note: There are something just cannot be roll back, // Note: There are something just cannot be roll back,
// so just keep error logs of those operations. // so just keep error logs of those operations.
path := UserPath(u.Name) path := UserPath(u.Name)
if err := util.RemoveAll(path); err != nil { if err = util.RemoveAll(path); err != nil {
return fmt.Errorf("Failed to RemoveAll %s: %v", path, err) err = fmt.Errorf("Failed to RemoveAll %s: %v", path, err)
_ = createNotice(e, NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
return err
} }
if len(u.Avatar) > 0 { if len(u.Avatar) > 0 {
avatarPath := u.CustomAvatarRelativePath() avatarPath := u.CustomAvatarRelativePath()
if err := storage.Avatars.Delete(avatarPath); err != nil { if err = storage.Avatars.Delete(avatarPath); err != nil {
return fmt.Errorf("Failed to remove %s: %v", avatarPath, err) err = fmt.Errorf("Failed to remove %s: %v", avatarPath, err)
_ = createNotice(e, NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
return err
} }
} }

View file

@ -6,6 +6,7 @@ package setting
import ( import (
"regexp" "regexp"
"time"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
) )
@ -50,7 +51,7 @@ var Service struct {
AutoWatchNewRepos bool AutoWatchNewRepos bool
AutoWatchOnChanges bool AutoWatchOnChanges bool
DefaultOrgMemberVisible bool DefaultOrgMemberVisible bool
UserDeleteWithCommentsMaxDays int UserDeleteWithCommentsMaxTime time.Duration
// OpenID settings // OpenID settings
EnableOpenIDSignIn bool EnableOpenIDSignIn bool
@ -103,7 +104,7 @@ func newService() {
Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes))
Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility]
Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool()
Service.UserDeleteWithCommentsMaxDays = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_DAYS").MustInt(0) Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0)
sec = Cfg.Section("openid") sec = Cfg.Section("openid")
Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock) Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock)

View file

@ -648,7 +648,7 @@ repos_none = You do not own any repositories
delete_account = Delete Your Account delete_account = Delete Your Account
delete_prompt = This operation will permanently delete your user account. It <strong>CAN NOT</strong> be undone. delete_prompt = This operation will permanently delete your user account. It <strong>CAN NOT</strong> be undone.
delete_with_all_comments = Your account is younger than %d days. To avoid ghost comments, all issue/PR comments will be deleted with it. delete_with_all_comments = Your account is younger than %s. To avoid ghost comments, all issue/PR comments will be deleted with it.
confirm_delete_account = Confirm Deletion confirm_delete_account = Confirm Deletion
delete_account_title = Delete User Account delete_account_title = Delete User Account
delete_account_desc = Are you sure you want to permanently delete this user account? delete_account_desc = Are you sure you want to permanently delete this user account?

View file

@ -509,7 +509,7 @@ func deleteIssueComment(ctx *context.APIContext) {
return return
} }
if err = comment_service.DeleteComment(comment, ctx.User); err != nil { if err = comment_service.DeleteComment(ctx.User, comment); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err) ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err)
return return
} }

View file

@ -2125,7 +2125,7 @@ func DeleteComment(ctx *context.Context) {
return return
} }
if err = comment_service.DeleteComment(comment, ctx.User); err != nil { if err = comment_service.DeleteComment(ctx.User, comment); err != nil {
ctx.ServerError("DeleteCommentByID", err) ctx.ServerError("DeleteCommentByID", err)
return return
} }

View file

@ -302,8 +302,8 @@ func loadAccountData(ctx *context.Context) {
ctx.Data["ActivationsPending"] = pendingActivation ctx.Data["ActivationsPending"] = pendingActivation
ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
if setting.Service.UserDeleteWithCommentsMaxDays != 0 { if setting.Service.UserDeleteWithCommentsMaxTime != 0 {
ctx.Data["UserDeleteWithCommentsMaxDays"] = setting.Service.UserDeleteWithCommentsMaxDays ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String()
ctx.Data["UserDeleteWithComments"] = ctx.User.CreatedUnix.AsTime().Add(time.Duration(setting.Service.UserDeleteWithCommentsMaxDays) * 24 * time.Hour).After(time.Now()) ctx.Data["UserDeleteWithComments"] = ctx.User.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now())
} }
} }

View file

@ -43,8 +43,8 @@ func UpdateComment(c *models.Comment, doer *models.User, oldContent string) erro
} }
// DeleteComment deletes the comment // DeleteComment deletes the comment
func DeleteComment(comment *models.Comment, doer *models.User) error { func DeleteComment(doer *models.User, comment *models.Comment) error {
if err := models.DeleteComment(comment, doer); err != nil { if err := models.DeleteComment(comment); err != nil {
return err return err
} }

View file

@ -174,7 +174,7 @@
<div class="ui red message"> <div class="ui red message">
<p class="text left">{{svg "octicon-alert"}} {{.i18n.Tr "settings.delete_prompt" | Str2html}}</p> <p class="text left">{{svg "octicon-alert"}} {{.i18n.Tr "settings.delete_prompt" | Str2html}}</p>
{{ if .UserDeleteWithComments }} {{ if .UserDeleteWithComments }}
<p class="text left" style="font-weight: bold;">{{.i18n.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxDays | Str2html}}</p> <p class="text left" style="font-weight: bold;">{{.i18n.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxTime | Str2html}}</p>
{{ end }} {{ end }}
</div> </div>
<form class="ui form ignore-dirty" id="delete-form" action="{{AppSubUrl}}/user/settings/account/delete" method="post"> <form class="ui form ignore-dirty" id="delete-form" action="{{AppSubUrl}}/user/settings/account/delete" method="post">