From 8d9588b039ff476207224101caba6c9598315b50 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 9 Sep 2024 21:17:22 -0700 Subject: [PATCH] Notification when admin users are blocked --- pkg/controller/admin/user_actions.go | 6 +++++ pkg/controller/block/block.go | 33 ++++++++++++++++++++++----- pkg/models/blocklist.go | 7 ++++++ pkg/models/user.go | 27 ++++++++++++++++++++++ web/templates/account/search.html | 8 ++++++- web/templates/admin/user_actions.html | 23 +++++++++++++++++++ 6 files changed, 97 insertions(+), 7 deletions(-) diff --git a/pkg/controller/admin/user_actions.go b/pkg/controller/admin/user_actions.go index d113a10..fb0ee1b 100644 --- a/pkg/controller/admin/user_actions.go +++ b/pkg/controller/admin/user_actions.go @@ -117,11 +117,17 @@ func UserActions() http.HandlerFunc { return } + // Get their block lists. insights, err := models.GetBlocklistInsights(user) if err != nil { session.FlashError(w, r, "Error getting blocklist insights: %s", err) } vars["BlocklistInsights"] = insights + + // Also surface counts of admin blocks. + count, total := models.CountBlockedAdminUsers(user) + vars["AdminBlockCount"] = count + vars["AdminBlockTotal"] = total case "essays": // Edit their profile essays easily. if !currentUser.HasAdminScope(config.ScopePhotoModerator) { diff --git a/pkg/controller/block/block.go b/pkg/controller/block/block.go index a73e365..d99e51d 100644 --- a/pkg/controller/block/block.go +++ b/pkg/controller/block/block.go @@ -112,17 +112,35 @@ func BlockUser() http.HandlerFunc { return } - // Can't block admins who have the unblockable scope. - if user.IsAdmin && user.HasAdminScope(config.ScopeUnblockable) { + // If the target user is an admin, log this to the admin reports page. + if user.IsAdmin { + // Is the target admin user unblockable? + var ( + unblockable = user.HasAdminScope(config.ScopeUnblockable) + footer string // qualifier for the admin report body + ) + + // Add a footer to the report to indicate whether the block goes through. + if unblockable { + footer = "**Unblockable:** this admin can not be blocked, so the block was not added and the user was shown an error message." + } else { + footer = "**Notice:** This admin is not unblockable, so the block has been added successfully." + } + + // Also, include this user's current count of blocked admin users. + count, total := models.CountBlockedAdminUsers(currentUser) + footer += fmt.Sprintf("\n\nThis user now blocks %d of %d admin user(s) on this site.", count+1, total) + // For curiosity's sake, log a report. fb := &models.Feedback{ Intent: "report", Subject: "A user tried to block an admin", Message: fmt.Sprintf( "A user has tried to block an admin user account!\n\n"+ - "* Username: %s\n* Tried to block: %s", + "* Username: %s\n* Tried to block: %s\n\n%s", currentUser.Username, user.Username, + footer, ), UserID: currentUser.ID, TableName: "users", @@ -132,9 +150,12 @@ func BlockUser() http.HandlerFunc { log.Error("Could not log feedback for user %s trying to block admin %s: %s", currentUser.Username, user.Username, err) } - session.FlashError(w, r, "You can not block site administrators.") - templates.Redirect(w, "/u/"+username) - return + // If the admin is unblockable, give the user an error message and return. + if unblockable { + session.FlashError(w, r, "You can not block site administrators.") + templates.Redirect(w, "/u/"+username) + return + } } // Block the target user. diff --git a/pkg/models/blocklist.go b/pkg/models/blocklist.go index 1e61b89..53f2d3f 100644 --- a/pkg/models/blocklist.go +++ b/pkg/models/blocklist.go @@ -199,6 +199,7 @@ func GetBlocklistInsights(user *User) (*BlocklistInsight, error) { reverse = []*Block{} // Users who block the target userIDs = []uint64{user.ID} usernames = map[uint64]string{} + admins = map[uint64]bool{} ) // Get the complete blocklist and bucket them into forward and reverse. @@ -218,6 +219,7 @@ func GetBlocklistInsights(user *User) (*BlocklistInsight, error) { type scanItem struct { ID uint64 Username string + IsAdmin bool } var scan = []scanItem{} if res := DB.Table( @@ -225,6 +227,7 @@ func GetBlocklistInsights(user *User) (*BlocklistInsight, error) { ).Select( "id", "username", + "is_admin", ).Where( "id IN ?", userIDs, ).Scan(&scan); res.Error != nil { @@ -233,6 +236,7 @@ func GetBlocklistInsights(user *User) (*BlocklistInsight, error) { for _, row := range scan { usernames[row.ID] = row.Username + admins[row.ID] = row.IsAdmin } } @@ -245,6 +249,7 @@ func GetBlocklistInsights(user *User) (*BlocklistInsight, error) { if username, ok := usernames[row.TargetUserID]; ok { result.Blocks = append(result.Blocks, BlocklistInsightUser{ Username: username, + IsAdmin: admins[row.TargetUserID], Date: row.CreatedAt, }) } @@ -253,6 +258,7 @@ func GetBlocklistInsights(user *User) (*BlocklistInsight, error) { if username, ok := usernames[row.SourceUserID]; ok { result.BlockedBy = append(result.BlockedBy, BlocklistInsightUser{ Username: username, + IsAdmin: admins[row.SourceUserID], Date: row.CreatedAt, }) } @@ -268,6 +274,7 @@ type BlocklistInsight struct { type BlocklistInsightUser struct { Username string + IsAdmin bool Date time.Time } diff --git a/pkg/models/user.go b/pkg/models/user.go index 0061e8e..5fc3027 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -595,6 +595,33 @@ func MapAdminUsers(user *User) (UserMap, error) { return MapUsers(user, userIDs) } +// CountBlockedAdminUsers returns a count of how many admin users the current user has blocked, out of how many total. +func CountBlockedAdminUsers(user *User) (count, total int64) { + // Count the blocked admins. + DB.Model(&User{}).Select( + "count(users.id) AS cnt", + ).Joins( + "JOIN blocks ON (blocks.target_user_id = users.id)", + ).Where( + "blocks.source_user_id = ? AND users.is_admin IS TRUE", + user.ID, + ).Count(&count) + + // And the total number of available admins. + total = CountAdminUsers() + return +} + +// CountAdminUsers returns a count of how many admin users exist on the site. +func CountAdminUsers() (count int64) { + DB.Model(&User{}).Select( + "count(id) AS cnt", + ).Where( + "users.is_admin IS TRUE", + ).Count(&count) + return +} + // Has a user ID in the map? func (um UserMap) Has(id uint64) bool { _, ok := um[id] diff --git a/web/templates/account/search.html b/web/templates/account/search.html index bbd9d22..7612f1c 100644 --- a/web/templates/account/search.html +++ b/web/templates/account/search.html @@ -100,11 +100,17 @@

It is also against the Terms of Service of this website, and - members who violate this rule will be banned. This website is actively monitored to keep on top of this stuff, + members who violate this rule will be banned. This website is actively monitored to keep on top of this stuff, and we cooperate enthusiastically with NCMEC and relevant law enforcement agencies.

+ +

+ This incident has been reported to the website administrator. If you are surprised to see this message and + it was on accident, don't worry -- but repeated attempts to bypass this search filter by trying + other related keywords will be noticed and may attract extra admin attention to your account. +

{{end}} diff --git a/web/templates/admin/user_actions.html b/web/templates/admin/user_actions.html index b5a0b92..fff5895 100644 --- a/web/templates/admin/user_actions.html +++ b/web/templates/admin/user_actions.html @@ -99,6 +99,23 @@

Block Lists

+ + {{if .AdminBlockCount}} +
+ + Blocked Admins {{.AdminBlockCount}} +
+ +

+ This user blocks {{.AdminBlockCount}} out of {{.AdminBlockTotal}} admin{{Pluralize64 .AdminBlockTotal}} of this website. +

+ +

+ If this number is unusually high, it can indicate this user may be proactively blocking all the admins in order to be + sneaky or evade moderation. +

+ {{end}} +
Forward List {{len .BlocklistInsights.Blocks}}
@@ -118,6 +135,9 @@ {{range .BlocklistInsights.Blocks}}
  • {{.Username}} + {{if .IsAdmin}} + + {{end}} {{.Date.Format "2006-01-02"}}
  • {{end}} @@ -141,6 +161,9 @@ {{range .BlocklistInsights.BlockedBy}}
  • {{.Username}} + {{if .IsAdmin}} + + {{end}} {{.Date.Format "2006-01-02"}}
  • {{end}}