From 20d04fc370d388ea6d640fdc7070525b88d5bee6 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Wed, 8 May 2024 21:03:31 -0700 Subject: [PATCH] Admin Transparency Page * Add a transparency page where regular user accounts can list the roles and permissions that an admin user has access to. It is available by clicking on the "Admin" badge on that user's profile page. * Add additional admin scopes to lock down more functionality: * User feedback and reports * Change logs * User notes and admin notes * Add friendly descriptions to what all the scopes mean in practice. * Don't show admin notification badges to admins who aren't allowed to act on those notifications. * Update the admin dashboard page and documentation for admins. --- pkg/config/admin_scopes.go | 45 +++- pkg/config/admin_scopes_test.go | 2 +- pkg/controller/account/user_note.go | 2 +- pkg/controller/admin/transparency.go | 43 ++++ pkg/router/router.go | 5 +- pkg/templates/template_funcs.go | 3 + pkg/templates/template_vars.go | 15 +- web/templates/account/my_user_notes.html | 2 +- web/templates/account/profile.html | 11 +- web/templates/account/user_notes.html | 4 +- web/templates/admin/dashboard.html | 307 +++++++++++++++-------- web/templates/admin/transparency.html | 167 ++++++++++++ 12 files changed, 491 insertions(+), 115 deletions(-) create mode 100644 pkg/controller/admin/transparency.go create mode 100644 web/templates/admin/transparency.html diff --git a/pkg/config/admin_scopes.go b/pkg/config/admin_scopes.go index 07c7508..c06654a 100644 --- a/pkg/config/admin_scopes.go +++ b/pkg/config/admin_scopes.go @@ -35,18 +35,51 @@ const ( ScopeUserInsight = "admin.user.insights" ScopeUserImpersonate = "admin.user.impersonate" ScopeUserBan = "admin.user.ban" - ScopeUserPromote = "admin.user.promote" ScopeUserDelete = "admin.user.delete" + ScopeUserPromote = "admin.user.promote" + + // Other admin views + ScopeFeedbackAndReports = "admin.feedback" + ScopeChangeLog = "admin.changelog" + ScopeUserNotes = "admin.user.notes" // Admins with this scope can not be blocked by users. ScopeUnblockable = "admin.unblockable" // Special scope to mark an admin automagically in the Inner Circle ScopeIsInnerCircle = "admin.override.inner-circle" + + // The global wildcard scope gets all available permissions. + ScopeSuperuser = "*" ) +// Friendly description for each scope. +var AdminScopeDescriptions = map[string]string{ + ScopeChatModerator: "Have operator controls in the chat room (can mark cameras as explicit, or kick/ban people from chat).", + ScopeForumModerator: "Ability to moderate the forum (edit or delete posts).", + ScopePhotoModerator: "Ability to moderate photo galleries (can see all private or friends-only photos, and edit or delete them).", + ScopeCircleModerator: "Ability to remove members from the inner circle.", + ScopeCertificationApprove: "Ability to see pending certification pictures and approve or reject them.", + ScopeCertificationList: "Ability to see existing certification pictures that have already been approved or rejected.", + ScopeCertificationView: "Ability to see and double check a specific user's certification picture on demand.", + ScopeForumAdmin: "Ability to manage forums themselves (add or remove forums, edit their properties).", + ScopeAdminScopeAdmin: "Ability to manage admin permissions for other admin accounts.", + ScopeMaintenance: "Ability to activate maintenance mode functions of the website (turn features on or off, disable signups or logins, etc.)", + ScopeUserInsight: "Ability to see admin insights about a user profile (e.g. their block lists and who blocks them).", + ScopeUserImpersonate: "Ability to log in as any user account (note: this action is logged and notifies all admins when it happens. Admins must write a reason and it is used to diagnose customer support issues, help with their certification picture, or investigate a reported Direct Message conversation they had).", + ScopeUserBan: "Ability to ban or unban user accounts.", + ScopeUserDelete: "Ability to fully delete user accounts on their behalf.", + ScopeUserPromote: "Ability to add or remove the admin status flag on a user profile.", + ScopeFeedbackAndReports: "Ability to see admin reports and user feedback.", + ScopeChangeLog: "Ability to see website change logs (e.g. history of a certification photo, gallery photo settings, etc.)", + ScopeUserNotes: "Ability to see all notes written about a user, or to see all notes written by admins.", + ScopeUnblockable: "This admin can not be added to user block lists.", + ScopeIsInnerCircle: "This admin is automatically part of the inner circle.", + ScopeSuperuser: "This admin has access to ALL admin features on the website.", +} + // Number of expected scopes for unit test and validation. -const QuantityAdminScopes = 16 +const QuantityAdminScopes = 20 // The specially named Superusers group. const AdminGroupSuperusers = "Superusers" @@ -63,12 +96,20 @@ func ListAdminScopes() []string { ScopeCertificationView, ScopeForumAdmin, ScopeAdminScopeAdmin, + ScopeMaintenance, ScopeUserInsight, ScopeUserImpersonate, ScopeUserBan, ScopeUserDelete, ScopeUserPromote, + ScopeFeedbackAndReports, + ScopeChangeLog, + ScopeUserNotes, ScopeUnblockable, ScopeIsInnerCircle, } } + +func AdminScopeDescription(scope string) string { + return AdminScopeDescriptions[scope] +} diff --git a/pkg/config/admin_scopes_test.go b/pkg/config/admin_scopes_test.go index 1d582f1..c50da0c 100644 --- a/pkg/config/admin_scopes_test.go +++ b/pkg/config/admin_scopes_test.go @@ -10,7 +10,7 @@ import ( // returned by the scope list function. func TestAdminScopesCount(t *testing.T) { var scopes = config.ListAdminScopes() - if len(scopes) != config.QuantityAdminScopes { + if len(scopes) != config.QuantityAdminScopes || len(scopes) != len(config.AdminScopeDescriptions) { t.Errorf( "The list of scopes returned by ListAdminScopes doesn't match the expected count. "+ "Expected %d, got %d", diff --git a/pkg/controller/account/user_note.go b/pkg/controller/account/user_note.go index 76ad901..4c7e0a3 100644 --- a/pkg/controller/account/user_note.go +++ b/pkg/controller/account/user_note.go @@ -201,7 +201,7 @@ func MyNotes() http.HandlerFunc { } // Admin notes? - if adminNotes && !currentUser.IsAdmin { + if adminNotes && !currentUser.HasAdminScope(config.ScopeUserNotes) { adminNotes = false } diff --git a/pkg/controller/admin/transparency.go b/pkg/controller/admin/transparency.go new file mode 100644 index 0000000..2b76c2a --- /dev/null +++ b/pkg/controller/admin/transparency.go @@ -0,0 +1,43 @@ +package admin + +import ( + "net/http" + + "code.nonshy.com/nonshy/website/pkg/config" + "code.nonshy.com/nonshy/website/pkg/models" + "code.nonshy.com/nonshy/website/pkg/templates" +) + +// Admin transparency page that lists the scopes and permissions an admin account has for all to see. +func Transparency() http.HandlerFunc { + tmpl := templates.Must("admin/transparency.html") + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var ( + username = r.PathValue("username") + ) + + // Get this user. + user, err := models.FindUser(username) + if err != nil { + templates.NotFoundPage(w, r) + return + } + + // Only for admin user accounts. + if !user.IsAdmin { + templates.NotFoundPage(w, r) + return + } + + // Template variables. + var vars = map[string]interface{}{ + "User": user, + "AdminScopes": config.ListAdminScopes(), + } + + if err := tmpl.Execute(w, r, vars); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) +} diff --git a/pkg/router/router.go b/pkg/router/router.go index abbaee0..ab91332 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -78,6 +78,7 @@ func New() http.Handler { mux.Handle("GET /admin/unimpersonate", middleware.LoginRequired(admin.Unimpersonate())) mux.Handle("GET /inner-circle", middleware.LoginRequired(account.InnerCircle())) mux.Handle("/inner-circle/invite", middleware.LoginRequired(account.InviteCircle())) + mux.Handle("GET /admin/transparency/{username}", middleware.LoginRequired(admin.Transparency())) // Certification Required. Pages that only full (verified) members can access. mux.Handle("GET /photo/gallery", middleware.CertRequired(photo.SiteGallery())) @@ -95,14 +96,14 @@ func New() http.Handler { mux.Handle("GET /admin", middleware.AdminRequired("", admin.Dashboard())) mux.Handle("/admin/scopes", middleware.AdminRequired("", admin.Scopes())) mux.Handle("/admin/photo/certification", middleware.AdminRequired("", photo.AdminCertification())) - mux.Handle("/admin/feedback", middleware.AdminRequired("", admin.Feedback())) + mux.Handle("/admin/feedback", middleware.AdminRequired(config.ScopeFeedbackAndReports, admin.Feedback())) mux.Handle("/admin/user-action", middleware.AdminRequired("", admin.UserActions())) mux.Handle("/admin/maintenance", middleware.AdminRequired(config.ScopeMaintenance, admin.Maintenance())) mux.Handle("/forum/admin", middleware.AdminRequired(config.ScopeForumAdmin, forum.Manage())) mux.Handle("/forum/admin/edit", middleware.AdminRequired(config.ScopeForumAdmin, forum.AddEdit())) mux.Handle("/inner-circle/remove", middleware.LoginRequired(account.RemoveCircle())) mux.Handle("/admin/photo/mark-explicit", middleware.AdminRequired(config.ScopePhotoModerator, admin.MarkPhotoExplicit())) - mux.Handle("GET /admin/changelog", middleware.AdminRequired("", admin.ChangeLog())) + mux.Handle("GET /admin/changelog", middleware.AdminRequired(config.ScopeChangeLog, admin.ChangeLog())) // JSON API endpoints. mux.HandleFunc("GET /v1/version", api.Version()) diff --git a/pkg/templates/template_funcs.go b/pkg/templates/template_funcs.go index 011eac9..a90bd67 100644 --- a/pkg/templates/template_funcs.go +++ b/pkg/templates/template_funcs.go @@ -71,6 +71,9 @@ func TemplateFuncs(r *http.Request) template.FuncMap { // Test if a photo should be blurred ({{BlurExplicit .Photo}}) "BlurExplicit": BlurExplicit(r), + + // Get a description for an admin scope (e.g. for transparency page). + "AdminScopeDescription": config.AdminScopeDescription, } } diff --git a/pkg/templates/template_vars.go b/pkg/templates/template_vars.go index f8d8a72..eab1e34 100644 --- a/pkg/templates/template_vars.go +++ b/pkg/templates/template_vars.go @@ -104,11 +104,20 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) { log.Error("MergeUserVars: couldn't CountFriendRequests for %d: %s", user.ID, err) } - // Are we admin? + // Are we admin? Add notification counts if the current admin can respond to them. if user.IsAdmin { + var countCertPhotos, countFeedback int64 + // Any pending certification photos or feedback? - countCertPhotos = models.CountCertificationPhotosNeedingApproval() - countFeedback = models.CountUnreadFeedback() + if user.HasAdminScope(config.ScopeCertificationApprove) { + countCertPhotos = models.CountCertificationPhotosNeedingApproval() + } + + // Admin feedback available? + if user.HasAdminScope(config.ScopeFeedbackAndReports) { + countFeedback = models.CountUnreadFeedback() + } + m["NavCertificationPhotos"] = countCertPhotos m["NavAdminFeedback"] = countFeedback diff --git a/web/templates/account/my_user_notes.html b/web/templates/account/my_user_notes.html index cc66489..26742cb 100644 --- a/web/templates/account/my_user_notes.html +++ b/web/templates/account/my_user_notes.html @@ -52,7 +52,7 @@ - {{if .CurrentUser.IsAdmin}} + {{if .CurrentUser.HasAdminScope "admin.user.notes"}}
diff --git a/web/templates/account/profile.html b/web/templates/account/profile.html index 057e4d0..25895fa 100644 --- a/web/templates/account/profile.html +++ b/web/templates/account/profile.html @@ -134,10 +134,13 @@ {{if .User.IsAdmin}}
- - - - Admin + + + + + Admin + +
{{end}} diff --git a/web/templates/account/user_notes.html b/web/templates/account/user_notes.html index 54ab7fc..8b798ab 100644 --- a/web/templates/account/user_notes.html +++ b/web/templates/account/user_notes.html @@ -123,7 +123,7 @@ - {{if .CurrentUser.IsAdmin}} + {{if .CurrentUser.HasAdminScope "admin.user.notes"}}

@@ -176,7 +176,7 @@

- {{if .CurrentUser.IsAdmin}} + {{if .CurrentUser.HasAdminScope "admin.feedback"}}
diff --git a/web/templates/admin/dashboard.html b/web/templates/admin/dashboard.html index 3b70f40..45b9015 100644 --- a/web/templates/admin/dashboard.html +++ b/web/templates/admin/dashboard.html @@ -12,105 +12,6 @@
-
-
-
-

- - Admin Guidelines NEW! -

-
- -
-

Respect the privacy of our users

- -
    -
  • - We do not snoop on our users' Direct Messages unless they report a conversation - for us to check out. The only way to access DMs is to impersonate a - user, which can't be done in secret. -
  • -
  • - We treat the Certification Photos as sensitive information. Go there only when a certification - photo is pending approval (red notification badges will guide the way). Do not download or leak - these images; be respectful. -
  • -
- -

What we moderate

- - Admin users are only expected to help moderate the following areas of the site: - -

1. User profile photos

- -

- Every picture uploaded to a user's profile page can be seen by admin users. The - admin gallery view can find all - user photos, whether private or friends-only, whether opted-in for the Site Gallery or not. -

- -

- Be careful not to "Like" or comment on a picture if the user marked it - "Friends only" or "Private" and they wouldn't expect you to have been able to see it. "Like" - and "Comment" buttons are hidden in the admin gallery view to reduce accidents but they are - functional on the user's own gallery page. -

- -

2. The Forums

- -

- Keep up with the newest forum posts and generally make sure - people aren't fighting or uploading inappropriate photos to one of the few photo boards. -

- -

3. Reported DMs only

- -

- If a user reports a Direct Message conversation - they're having, a link to view that chat thread will be available from the report. - This will impersonate the reporter and will be logged - - see "Impersonating users," below. -

- -

- DMs are text-based only, so users won't be sending any image attachments that need - moderating and their privacy is to be respected. A user may report a problematic - conversation for us to take a look at. -

- -
- -

Impersonating users

- -

- From a user's profile page you can "impersonate," or log in as them. You should almost - never need to do this, and only to diagnose a reported issue from their account or - something like that. -

- -

- You will need to write a reason for impersonating a user. The event is - logged and will be e-mailed to the admin team along with your reason. The admin team is - alerted any time an Impersonate action is performed. -

- -

- Note: when you impersonate, their "Last logged in at" time is not updated by your actions. - So if a user were last seen a month ago, they will still appear last seen a month ago. - But other interactions you make as their account (e.g. marking notifications read, reading - their unread DMs) will work and may be noticed. -

- -
- -

- -

- -
-
-
-
+ +
+
+
+

+ + Admin Guidelines +

+
+ +
+

+ Table of contents: +

+ + +

What is my role as an admin?

+ +

+ Please see your Admin Transparency page to + review the list of permissions and roles you are capable of. Then, review the relevant sections + below for some guidelines and information relating to that role. +

+ +

Respect the privacy of our users

+ +
    +
  • + We do not snoop on our users' Direct Messages unless they report a conversation + for us to check out. The only way to access DMs is to impersonate a + user, which can't be done in secret. +
  • +
  • + We treat the Certification Photos as sensitive information. Go there only when a certification + photo is pending approval (green notification badges will guide the way). Do not download or leak + these images; be respectful. +
  • +
+ +

What we moderate

+ + Admin users are only expected to help moderate the following areas of the site: + +

1. User photo galleries

+ +

+ Every picture uploaded to a user's profile page can be seen by (some) admin users. The + admin gallery view can find all + user photos, whether private or friends-only, whether opted-in for the Site Gallery or not. +

+ +

+ Notice: the website will not allow you to accidentally "like" a photo + that you should not have been able to see. "Like" buttons are hidden during the admin view + of the Site Gallery, but in case you click it on their profile page, the website will display + an error message "You aren't allowed to like that photo" so the user isn't alarmed. +

+ +

2. The Forums

+ +

+ Keep up with the newest forum posts and generally make sure + people aren't fighting or uploading inappropriate photos to one of the few photo boards. +

+ +

3. The Chat Room

+ +

+ If you are moderating the chat rooms, your main responsibilities are to: +

+ +
    +
  • + Ensure that people mark their webcams as 'Explicit' / red if they are jerking off + or being sexual. Use the /nsfw command to set their camera to + red. +
      +
    1. + The first action is to mark their camera red for them - sometimes people just + forget! +
    2. +
    3. + If the user fights you and removes the 'explicit' flag on their camera, you + may send them a verbal warning or kick them from the room. Please wait until + they have fought your flag at least two times: sometimes users + are confused by the wording of the "your cam was marked explicit" message and + accidentally un-mark their camera. +
    4. +
    5. + If users are being difficult and insisting on keeping an explicit camera blue, + kicking or banning them from the room is OK. +
    6. +
    +
  • +
  • + Ensure that people are behaving themselves: not spamming or breaking the rules + by posting explicit photos in non-explicit channels. As above: a gentle warning is + often the first step before you kick or ban problematic people. +
  • +
+ +

4. Reported DMs only

+ +

+ If a user reports a Direct Message conversation + they're having, a link to view that chat thread will be available from the report. + This will impersonate the reporter and will be logged + - see "Impersonating users," below. +

+ +

+ DMs are text-based only, so users won't be sending any image attachments that need + moderating and their privacy is to be respected. A user may report a problematic + conversation for us to take a look at. +

+ +
+ +

Impersonating users

+ +

+ From a user's profile page you can "impersonate," or log in as them. You should almost + never need to do this, and only to diagnose a reported issue from their account or + something like that. +

+ +

+ You will need to write a reason for impersonating a user. The event is + logged and will be e-mailed to the admin team along with your reason. The admin team is + alerted any time an Impersonate action is performed. +

+ +

+ Note: when you impersonate, their "Last logged in at" time is not updated by your actions. + So if a user were last seen a month ago, they will still appear last seen a month ago. + But other interactions you make as their account (e.g. marking notifications read, reading + their unread DMs) will work and may be noticed. +

+ +
+ +

Chat room commands

+ +

+ If you are moderating the chat room, these are useful commands to know. You can type + these commands into the message box on the room. +

+ +
+
/help
+
Shows a reminder in chat about the available operator commands.
+ +
/nsfw username
+
Mark a user's webcam as 'explicit' on their behalf (turning it from a blue cam into a red cam).
+ +
/kick username
+
Kick a user from the chat room. They will be able to log back in.
+ +
/ban username
+
Temporarily ban the user from the chat room for 24 hours (by default).
+ +
/ban username hours
+
Provide a number of hours to set a different ban duration than the default (see examples below).
+ +
/bans
+
Get a list of currently active chat room bans.
+ +
/unban username
+
Immediately remove the ban flag on this username.
+
+ +

+ Important: + + The /help command lists some additional options that you generally should + never need to use. Be very careful not to issue commands like shutdown + which will reboot the chat server, as this can be very disruptive to our members. +

+ +

+ Generally, as a volunteer chat moderator you will only be using the /nsfw, /kick and /ban + commands as needed. +

+ +

Examples

+ +
    +
  • /kick {{.CurrentUser.Username}}
  • +
  • /ban {{.CurrentUser.Username}}
  • +
  • /ban {{.CurrentUser.Username}} 12
  • +
+ +

+ Note: the @ symbol should NOT appear in front of the username, the chat server currently + won't recognize the user. If you use the @ mention auto-complete, be sure to remove the @ + symbol before sending the command. This is a bug that will probably be fixed soon. +

+ +
+
+
+
diff --git a/web/templates/admin/transparency.html b/web/templates/admin/transparency.html new file mode 100644 index 0000000..f1315a3 --- /dev/null +++ b/web/templates/admin/transparency.html @@ -0,0 +1,167 @@ +{{define "title"}}Admin Transparency for: {{.User.Username}}{{end}} +{{define "content"}} +
+
+
+
+

+ Admin Transparency +

+

Scopes & permissions available to: {{.User.Username}}

+
+
+
+ + {{$Root := .}} + +
+

+ This web page provides transparency for the website administrators and what their specific + responsibilities and capabilities are. +

+ +

+ Administrators on {{PrettyTitle}} do not automatically have access to "god mode" powers to + use every admin feature across the entire website. Instead, admin accounts are assigned to + specific limited roles with related, narrowly scoped, permissions related to that role. + For example: an admin who only moderates the chat room will not have access + to see certification pictures or your private gallery pictures. +

+ +

+ This enables {{PrettyTitle}} to recruit help from volunteer moderators to help with very + specific tasks (such as chat room or forum moderation) while keeping their permissions locked + down so they can't access other sensitive areas of the admin website. +

+
+ +
+ +
+
+ +
+ +
+ +
+
+ {{template "avatar-64x64" .User}} +
+
+

{{.User.NameOrUsername}}

+

+ + {{.User.Username}} + + Admin + +

+
+
+ +
+ +

+ Admin accounts on {{PrettyTitle}} are assigned permissions based on the "groups" + they are in: each group relates to a specific role (such as chat moderator) and + grants the specific website permissions related to that role. +

+ +

+ @{{.User.Username}} is a member of {{len .User.AdminGroups}} admin group{{Pluralize (len .User.AdminGroups)}}: +

+ + {{if eq (len .User.AdminGroups) 0}} +
+ They are not assigned to any admin groups and so they have no special permissions aside + from the 'Admin' badge appearing on their profile page. +
+ {{end}} + + {{range .User.AdminGroups}} +
+

{{.Name}}

+ +

+ Permission scopes: +

+ +
+ {{range .Scopes}} +
+ + {{.Scope}} + {{if eq .Scope "*"}} + (wildcard scope that grants all permissions) + {{end}} + +
+
{{AdminScopeDescription .Scope}}
+ {{end}} +
+ {{end}} + +
+ +
+
+ +
+ +
+ +
+ +

+ For context to the above, the following is the complete and exhaustive list of + {{PrettyTitle}} admin capabilities that could be granted to an admin account. +

+ +

+ Permissions that this admin has will be highlighted in + green, + and permissions they do not have will be in + red. +

+ +
+ +
+ {{range .AdminScopes}} +
+ {{if $Root.User.HasAdminScope .}} + + + {{.}} + + {{else}} + + + {{.}} + + {{end}} +
+
{{AdminScopeDescription .}}
+ {{end}} +
+ +
+ +
+
+ +
+
+
+ +
+{{end}} \ No newline at end of file