discourse/app/controllers/user_badges_controller.rb
Alan Guo Xiang Tan 65820e8ac1
SECURITY: Restrict display of topic titles associated with user badges (#18768) (#18770)
Before this commit, we did not have guardian checks in place to determine if a
topic's title associated with a user badge should be displayed or not.
This means that the topic title of topics with restricted access
could be leaked to anon and users without access if certain conditions
are met. While we will not specify the conditions required, we have internally
assessed that the odds of meeting such conditions are low.

With this commit, we will now apply a guardian check to ensure that the
current user is able to see a topic before the topic's title is included
in the serialized object of a `UserBadge`.
2022-10-27 11:48:00 +08:00

165 lines
5.1 KiB
Ruby

# frozen_string_literal: true
class UserBadgesController < ApplicationController
MAX_BADGES = 96 # This was limited in PR#2360 to make it divisible by 8
before_action :ensure_badges_enabled
def index
params.permit [:granted_before, :offset, :username]
badge = fetch_badge_from_params
user_badges = badge.user_badges.order('granted_at DESC, id DESC').limit(MAX_BADGES)
user_badges = user_badges.includes(:user, :granted_by, badge: :badge_type, post: :topic, user: [:primary_group, :flair_group])
grant_count = nil
if params[:username]
user_id = User.where(username_lower: params[:username].downcase).pluck_first(:id)
user_badges = user_badges.where(user_id: user_id) if user_id
grant_count = badge.user_badges.where(user_id: user_id).count
end
if offset = params[:offset]
user_badges = user_badges.offset(offset.to_i)
end
user_badges_topic_ids = user_badges.map { |user_badge| user_badge.post&.topic_id }.compact
user_badges = UserBadges.new(user_badges: user_badges,
username: params[:username],
grant_count: grant_count)
render_serialized(
user_badges,
UserBadgesSerializer,
root: :user_badge_info,
include_long_description: true,
allowed_user_badge_topic_ids: guardian.can_see_topic_ids(topic_ids: user_badges_topic_ids)
)
end
def username
params.permit [:grouped]
user = fetch_user_from_params(include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts))
raise Discourse::NotFound unless guardian.can_see_profile?(user)
user_badges = user.user_badges
if params[:grouped]
user_badges = user_badges.group(:badge_id).select_for_grouping
end
user_badges = user_badges.includes(badge: [:badge_grouping, :badge_type, :image_upload])
.includes(post: :topic)
.includes(:granted_by)
user_badges_topic_ids = user_badges.map { |user_badge| user_badge.post&.topic_id }.compact
render_serialized(
user_badges,
DetailedUserBadgeSerializer,
allowed_user_badge_topic_ids: guardian.can_see_topic_ids(topic_ids: user_badges_topic_ids),
root: :user_badges,
)
end
def create
params.require(:username)
user = fetch_user_from_params
unless can_assign_badge_to_user?(user)
return render json: failed_json, status: 403
end
badge = fetch_badge_from_params
post_id = nil
if params[:reason].present?
unless is_badge_reason_valid? params[:reason]
return render json: failed_json.merge(message: I18n.t('invalid_grant_badge_reason_link')), status: 400
end
if route = Discourse.route_for(params[:reason])
if route[:controller] == "topics" && route[:action] == "show"
topic_id = (route[:id] || route[:topic_id]).to_i
post_number = route[:post_number] || 1
post_id = Post.find_by(topic_id: topic_id, post_number: post_number)&.id if topic_id > 0
end
end
end
user_badge = BadgeGranter.grant(badge, user, granted_by: current_user, post_id: post_id)
render_serialized(user_badge, DetailedUserBadgeSerializer, root: "user_badge")
end
def destroy
params.require(:id)
user_badge = UserBadge.find(params[:id])
unless can_assign_badge_to_user?(user_badge.user)
render json: failed_json, status: 403
return
end
BadgeGranter.revoke(user_badge, revoked_by: current_user)
render json: success_json
end
def toggle_favorite
params.require(:user_badge_id)
user_badge = UserBadge.find(params[:user_badge_id])
user_badges = user_badge.user.user_badges
unless can_favorite_badge?(user_badge)
return render json: failed_json, status: 403
end
if !user_badge.is_favorite && user_badges.select(:badge_id).distinct.where(is_favorite: true).count >= SiteSetting.max_favorite_badges
return render json: failed_json, status: 400
end
UserBadge
.where(user_id: user_badge.user_id, badge_id: user_badge.badge_id)
.update_all(is_favorite: !user_badge.is_favorite)
UserBadge.update_featured_ranks!(user_badge.user_id)
end
private
# Get the badge from either the badge name or id specified in the params.
def fetch_badge_from_params
badge = nil
params.permit(:badge_name)
if params[:badge_name].nil?
params.require(:badge_id)
badge = Badge.find_by(id: params[:badge_id], enabled: true)
else
badge = Badge.find_by(name: params[:badge_name], enabled: true)
end
raise Discourse::NotFound if badge.blank?
badge
end
def can_assign_badge_to_user?(user)
master_api_call = current_user.nil? && is_api?
master_api_call || guardian.can_grant_badges?(user)
end
def can_favorite_badge?(user_badge)
current_user == user_badge.user && !(1..4).include?(user_badge.badge_id)
end
def ensure_badges_enabled
raise Discourse::NotFound unless SiteSetting.enable_badges?
end
def is_badge_reason_valid?(reason)
route = Discourse.route_for(reason)
route && (route[:controller] == 'posts' || route[:controller] == 'topics')
end
end