Prior to this commit the following situation existed: > The path /dashboard/analytics/org/:id requires user > authentication (e.g. signed in). However, it does not enforce > authorization. Anyone can see this page. The page, however, uses > javascript to populate the data. So no information, aside from the org > name associated with the :id leaks out. The javascript API end point > enforces organization membership. > > I would expect that the authorization in the HTML rendering would be > the same as the javascript API end point. This commit ensures that the dashboards#analytics end point uses the same policy logic as the API analytics end points. Further, it keeps folks who aren't org members out of the base HTML page for other orgs. Closes forem/forem/#16985
128 lines
4.4 KiB
Ruby
128 lines
4.4 KiB
Ruby
# @note The actions of this class are overloaded with three concerns:
|
|
#
|
|
# - the current user
|
|
# - a given user
|
|
# - a given organization
|
|
#
|
|
# The implementation details are such that things silently "fallback" to the current user's
|
|
# information. This fallback happens when we have quasi-policy checks say the current user
|
|
# can't access the given user or given organization.
|
|
#
|
|
# [@jeremyf] I'm including these notes for future refactors, as I've spent time trying to
|
|
# improve legibility of the code but there are logical assumptions that require revisiting
|
|
# (hence https://github.com/forem/forem/issues/16931).
|
|
class DashboardsController < ApplicationController
|
|
before_action :set_no_cache_header
|
|
before_action :authenticate_user!
|
|
|
|
LIMIT_PER_PAGE_DEFAULT = 80
|
|
LIMIT_PER_PAGE_MAX = 1000
|
|
def show
|
|
fetch_and_authorize_user
|
|
target = @user
|
|
# NOTE: This is a subtle policy check happening here that we are not encapsulating
|
|
not_authorized if params[:org_id] && !@user.org_admin?(params[:org_id] || @user.any_admin?)
|
|
|
|
@organizations = @user.admin_organizations
|
|
|
|
# NOTE: This logic is a super set of the above not_authorized check
|
|
if params[:which] == "organization" && params[:org_id] && (@user.org_admin?(params[:org_id]) || @user.any_admin?)
|
|
target = @organizations.find_by(id: params[:org_id])
|
|
@organization = target
|
|
@articles = target.articles
|
|
else
|
|
# This redirect assumes that the dashboards#show action renders article specific information.
|
|
# When a user doesn't have articles nor can they create them, we want to send them somewhere
|
|
# else.
|
|
redirect_to dashboard_following_tags_path unless policy(Article).has_existing_articles_or_can_create_new_ones?
|
|
|
|
# if the target is a user, we need to eager load the organization
|
|
@articles = target.articles.includes(:organization)
|
|
end
|
|
|
|
@reactions_count = @articles.sum(&:public_reactions_count)
|
|
@page_views_count = @articles.sum(&:page_views_count)
|
|
|
|
@articles = @articles.includes(:collection).sorting(params[:sort]).decorate
|
|
@articles = Kaminari.paginate_array(@articles).page(params[:page]).per(50)
|
|
@collections_count = target.collections.non_empty.count
|
|
end
|
|
|
|
def following_tags
|
|
fetch_and_authorize_user
|
|
@followed_tags = follows_for(user: @user, type: "ActsAsTaggableOn::Tag", order_by: :points)
|
|
end
|
|
|
|
def following_users
|
|
fetch_and_authorize_user
|
|
@follows = follows_for(user: @user, type: "User")
|
|
end
|
|
|
|
def following_organizations
|
|
fetch_and_authorize_user
|
|
@followed_organizations = follows_for(user: @user, type: "Organization")
|
|
end
|
|
|
|
def following_podcasts
|
|
fetch_and_authorize_user
|
|
@followed_podcasts = follows_for(user: @user, type: "Podcast")
|
|
end
|
|
|
|
def followers
|
|
fetch_and_authorize_user
|
|
@follows = Follow.followable_user(@user.id)
|
|
.includes(:follower).order(created_at: :desc).limit(follows_limit)
|
|
end
|
|
|
|
def analytics
|
|
@user_or_org = if params[:org_id]
|
|
Organization.find(params[:org_id])
|
|
else
|
|
current_user
|
|
end
|
|
authorize(@user_or_org, :analytics?)
|
|
@organizations = current_user.member_organizations
|
|
end
|
|
|
|
def subscriptions
|
|
fetch_and_authorize_user
|
|
set_source
|
|
authorize @source
|
|
@subscriptions = @source.user_subscriptions
|
|
.includes(:subscriber).order(created_at: :desc).page(params[:page]).per(100)
|
|
end
|
|
|
|
private
|
|
|
|
def follows_for(user:, type:, order_by: :created_at)
|
|
user.follows_by_type(type).order(order_by => :desc).includes(:followable).limit(follows_limit)
|
|
end
|
|
|
|
def set_source
|
|
source_type = UserSubscription::ALLOWED_TYPES.detect { |allowed_type| allowed_type == params[:source_type] }
|
|
|
|
not_found unless source_type
|
|
|
|
source = source_type.constantize.find_by(id: params[:source_id])
|
|
@source = source || not_found
|
|
end
|
|
|
|
def fetch_and_authorize_user
|
|
@user = if params[:username] && current_user.any_admin?
|
|
User.find_by(username: params[:username])
|
|
else
|
|
current_user
|
|
end
|
|
# NOTE: later we expect @user so the `||` is a bit misleading.
|
|
authorize (@user || User), :dashboard_show?
|
|
end
|
|
|
|
def follows_limit(default: LIMIT_PER_PAGE_DEFAULT, max: LIMIT_PER_PAGE_MAX)
|
|
return default unless params.key?(:per_page)
|
|
|
|
per_page = params[:per_page].to_i
|
|
return max if per_page > max
|
|
|
|
per_page
|
|
end
|
|
end
|