From f5221e0aec6d0bbb573db685f16a2d5f6446b1c9 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotlarek <kotlarek.krzysztof@gmail.com> Date: Wed, 13 Nov 2024 14:04:20 +1100 Subject: [PATCH] SECURITY: Moderators cannot see user emails. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unless `moderators_view_emails` SiteSetting is enabled, moderators should not be able to discover users’ emails. --- .../admin/addon/templates/logs.hbs | 10 +++--- .../app/lib/sidebar/admin-nav-map.js | 3 +- .../app/lib/sidebar/admin-sidebar.js | 12 +++++-- .../admin/screened_emails_controller.rb | 6 ++++ app/serializers/current_user_serializer.rb | 11 +++++- lib/guardian.rb | 6 ++++ .../admin/screened_emails_controller_spec.rb | 36 ++++++++++++++++--- spec/requests/export_csv_controller_spec.rb | 17 +++++++++ spec/system/admin_sidebar_navigation_spec.rb | 23 ++++++++++++ 9 files changed, 112 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/admin/addon/templates/logs.hbs b/app/assets/javascripts/admin/addon/templates/logs.hbs index e530f3cc507..854b27d01cf 100644 --- a/app/assets/javascripts/admin/addon/templates/logs.hbs +++ b/app/assets/javascripts/admin/addon/templates/logs.hbs @@ -3,10 +3,12 @@ @route="adminLogs.staffActionLogs" @label="admin.logs.staff_actions.title" /> - <NavItem - @route="adminLogs.screenedEmails" - @label="admin.logs.screened_emails.title" - /> + {{#if this.currentUser.can_see_emails}} + <NavItem + @route="adminLogs.screenedEmails" + @label="admin.logs.screened_emails.title" + /> + {{/if}} <NavItem @route="adminLogs.screenedIpAddresses" @label="admin.logs.screened_ips.title" diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/admin-nav-map.js b/app/assets/javascripts/discourse/app/lib/sidebar/admin-nav-map.js index 6a830a6d3aa..16b226fa34a 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/admin-nav-map.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/admin-nav-map.js @@ -1,5 +1,6 @@ import getURL from "discourse-common/lib/get-url"; +export const LOGS_SCREENED_EMAILS_LINK_KEY = "admin_logs_screened_emails"; export const ADMIN_NAV_MAP = [ { name: "account", @@ -246,7 +247,7 @@ export const ADMIN_NAV_MAP = [ icon: "external-link-alt", }, { - name: "admin_logs_screened_emails", + name: LOGS_SCREENED_EMAILS_LINK_KEY, route: "adminLogs.screenedEmails", label: "admin.security.sidebar_link.screened_emails", icon: "envelope", diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/admin-sidebar.js b/app/assets/javascripts/discourse/app/lib/sidebar/admin-sidebar.js index 483d47a04e2..b13fd984517 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/admin-sidebar.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/admin-sidebar.js @@ -1,7 +1,10 @@ import { cached } from "@glimmer/tracking"; import { htmlSafe } from "@ember/template"; import PreloadStore from "discourse/lib/preload-store"; -import { ADMIN_NAV_MAP } from "discourse/lib/sidebar/admin-nav-map"; +import { + ADMIN_NAV_MAP, + LOGS_SCREENED_EMAILS_LINK_KEY, +} from "discourse/lib/sidebar/admin-nav-map"; import BaseCustomSidebarPanel from "discourse/lib/sidebar/base-custom-sidebar-panel"; import BaseCustomSidebarSection from "discourse/lib/sidebar/base-custom-sidebar-section"; import BaseCustomSidebarSectionLink from "discourse/lib/sidebar/base-custom-sidebar-section-link"; @@ -340,7 +343,12 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel { if (!currentUser.admin && currentUser.moderator) { navConfig.forEach((section) => { - section.links = section.links.filterBy("moderator"); + section.links = section.links.filter((link) => { + if (link.name === LOGS_SCREENED_EMAILS_LINK_KEY) { + return siteSettings.moderators_view_emails; + } + return link.moderator; + }); }); navConfig = navConfig.filterBy("links.length"); } diff --git a/app/controllers/admin/screened_emails_controller.rb b/app/controllers/admin/screened_emails_controller.rb index b8688a0998d..e38175cbe26 100644 --- a/app/controllers/admin/screened_emails_controller.rb +++ b/app/controllers/admin/screened_emails_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::ScreenedEmailsController < Admin::StaffController + before_action :ensure_can_see_emails + def index screened_emails = ScreenedEmail.limit(200).order("last_match_at desc").to_a render_serialized(screened_emails, ScreenedEmailSerializer) @@ -11,4 +13,8 @@ class Admin::ScreenedEmailsController < Admin::StaffController screen.destroy! render json: success_json end + + def ensure_can_see_emails + guardian.ensure_can_see_emails! + end end diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index 9d9dccf0955..9ec2e31ec70 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -76,7 +76,8 @@ class CurrentUserSerializer < BasicUserSerializer :can_view_raw_email, :use_glimmer_topic_list?, :login_method, - :render_experimental_about_page + :render_experimental_about_page, + :can_see_emails delegate :user_stat, to: :object, private: true delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat @@ -320,4 +321,12 @@ class CurrentUserSerializer < BasicUserSerializer def use_glimmer_topic_list? scope.user.in_any_groups?(SiteSetting.experimental_glimmer_topic_list_groups_map) end + + def can_see_emails + scope.can_see_emails? + end + + def include_can_see_emails? + object.staff? + end end diff --git a/lib/guardian.rb b/lib/guardian.rb index 523b398dd0f..dbd212ebfa7 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -534,6 +534,7 @@ class Guardian def can_export_entity?(entity) return false if anonymous? return true if is_admin? + return can_see_emails? if entity == "screened_email" return entity != "user_list" if is_moderator? # Regular users can only export their archives @@ -544,6 +545,11 @@ class Guardian ).count == 0 end + def can_see_emails? + return true if is_admin? + SiteSetting.moderators_view_emails && is_moderator? + end + def can_mute_user?(target_user) can_mute_users? && @user.id != target_user.id && !target_user.staff? end diff --git a/spec/requests/admin/screened_emails_controller_spec.rb b/spec/requests/admin/screened_emails_controller_spec.rb index e8477784232..bfecb1c459a 100644 --- a/spec/requests/admin/screened_emails_controller_spec.rb +++ b/spec/requests/admin/screened_emails_controller_spec.rb @@ -23,8 +23,11 @@ RSpec.describe Admin::ScreenedEmailsController do include_examples "screened emails accessible" end - context "when logged in as a moderator" do - before { sign_in(moderator) } + context "when logged in as a moderator and has permission to view emails" do + before do + sign_in(moderator) + SiteSetting.moderators_view_emails = true + end include_examples "screened emails accessible" end @@ -39,6 +42,17 @@ RSpec.describe Admin::ScreenedEmailsController do expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end + + context "when logged in as a moderator but no permission to view emails" do + before { sign_in(moderator) } + + it "denies access with a 403 response" do + get "/admin/logs/screened_emails.json" + + expect(response.status).to eq(403) + expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) + end + end end describe "#destroy" do @@ -58,8 +72,11 @@ RSpec.describe Admin::ScreenedEmailsController do include_examples "screened email deletion possible" end - context "when logged in as a moderator" do - before { sign_in(moderator) } + context "when logged in as a moderator and has permission to view emails" do + before do + sign_in(moderator) + SiteSetting.moderators_view_emails = true + end include_examples "screened email deletion possible" end @@ -74,5 +91,16 @@ RSpec.describe Admin::ScreenedEmailsController do expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end + + context "when logged in as a moderator but no permission to view emails" do + before { sign_in(moderator) } + + it "prevents deletion with a 403 response" do + delete "/admin/logs/screened_emails/#{screened_email.id}.json" + + expect(response.status).to eq(403) + expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) + end + end end end diff --git a/spec/requests/export_csv_controller_spec.rb b/spec/requests/export_csv_controller_spec.rb index d55106cb60f..b54f90452ae 100644 --- a/spec/requests/export_csv_controller_spec.rb +++ b/spec/requests/export_csv_controller_spec.rb @@ -97,6 +97,23 @@ RSpec.describe ExportCsvController do expect(response.status).to eq(422) end + it "does not allow moderators to export screened_email if they has no permission to view emails" do + SiteSetting.moderators_view_emails = false + post "/export_csv/export_entity.json", params: { entity: "screened_email" } + expect(response.status).to eq(422) + end + + it "allows moderator to export screened_email if they has permission to view emails" do + SiteSetting.moderators_view_emails = true + post "/export_csv/export_entity.json", params: { entity: "screened_email" } + expect(response.status).to eq(200) + expect(response.parsed_body["success"]).to eq("OK") + + job_data = Jobs::ExportCsvFile.jobs.first["args"].first + expect(job_data["entity"]).to eq("screened_email") + expect(job_data["user_id"]).to eq(moderator.id) + end + it "allows moderator to export other entities" do post "/export_csv/export_entity.json", params: { entity: "staff_action" } expect(response.status).to eq(200) diff --git a/spec/system/admin_sidebar_navigation_spec.rb b/spec/system/admin_sidebar_navigation_spec.rb index 84ebdac0d99..7b9788fa227 100644 --- a/spec/system/admin_sidebar_navigation_spec.rb +++ b/spec/system/admin_sidebar_navigation_spec.rb @@ -274,6 +274,29 @@ describe "Admin Revamp | Sidebar Navigation", type: :system do sidebar.toggle_all_sections + links = page.all(".sidebar-section-link-content-text") + expect(links.map(&:text)).to eq( + [ + "Dashboard", + "Users", + "What's New", + "All", + "Watched Words", + "Screened IPs", + "Screened URLs", + "Search Logs", + "Staff Action Logs", + ], + ) + end + + it "displays limited links for moderator with screened emails if allowed" do + SiteSetting.moderators_view_emails = true + sign_in(moderator) + visit("/admin") + + sidebar.toggle_all_sections + links = page.all(".sidebar-section-link-content-text") expect(links.map(&:text)).to eq( [