docbrown/app/policies/article_policy.rb
Joshua Wehner 445fd0f9e5
Try renaming moderator -> super_moderator (#18261)
* Try renaming moderator -> super_moderator

* Still finding 'moderator' words

* Fixes for failing specs

* Update test with new role name

* Update app/services/moderator/manage_activity_and_roles.rb

Co-authored-by: Suzanne Aitchison <suzanne@forem.com>

Co-authored-by: Suzanne Aitchison <suzanne@forem.com>
2022-08-10 16:43:30 +02:00

261 lines
9.7 KiB
Ruby

class ArticlePolicy < ApplicationPolicy
MAX_TAG_LIST_SIZE = 4
# @return [TrueClass] when only Forem admins can post an Article.
# @return [FalseClass] when most any Forem user can post an Article.
#
# @note This is for Authorization System: use case 1-1. At present, this is the quickest way to
# refactor our code to deliver on that feature.
#
# @see https://github.com/forem/forem/pull/16437 for pattern of adding a predicate method to the
# "most relevant" class.
# @see https://github.com/orgs/forem/projects/46 for project details
def self.limit_post_creation_to_admins?
FeatureFlag.enabled?(:limit_post_creation_to_admins)
end
# @param query [Symbol] the name of one of the ArticlePolicy action predicates (e.g. :create?,
# :new?) though as a convenience, we will also accept :new, and :create.
# @return [TrueClass] if this query should default to hidden
# @return [FalseClass] if this query should not be hidden in the UI.
#
# @note The symmetry of the case statement structure with .scope_users_authorized_to_action
def self.include_hidden_dom_class_for?(query:)
case query.to_sym
when :create?, :new?, :create, :new
limit_post_creation_to_admins?
else
false
end
end
# Helps filter a `:users_scope` to those authorized to the `:action`. I want a list of all users
# who can create an Article. This policy method can help with that.
#
# @param users_scope [ActiveRecord::Relation] a scope for querying user objects
# @param action [Symbol] the name of one of the ArticlePolicy action predicates (e.g. :create?,
# :new?) though as a convenience, we will also accept :new, and :create.
#
# @return [ActiveRecord::Relation]
#
# @see https://api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/ClassMethods.html#method-i-scope
#
# @note With this duplication it would be feasible to alter the instance method logics to use the
# class method (e.g. `ArticlePolicy.scope_authorized(users_scope: User, action:
# :create?).find_by(user.id)`) but that's a future consideration.
#
# @note This is not a Pundit scope (see https://github.com/varvet/pundit#scopes), as those methods
# are for answering "What articles can I see?" This method is for answering "Who all can
# <action> on Articles?"
#
# @note Why isn't this a User.scope method? Because the logic of who can take an action on the
# resource is the problem domain of the policy.
#
# @note The symmetry of the case statement structure with .include_hidden_dom_class_for?
def self.scope_users_authorized_to_action(users_scope:, action:)
case action.to_sym
when :create?, :new?, :create, :new
# Note the delicate dance to duplicate logic in a general sense. [I hope that] this is a
# stop-gap solution.
users_scope = users_scope.without_role(:suspended)
return users_scope unless limit_post_creation_to_admins?
# NOTE: Not a fan of reaching over to the constant of another class, but I digress.
users_scope.with_any_role(*Authorizer::RoleBasedQueries::ANY_ADMIN_ROLES)
else
# Not going to implement all of the use cases.
raise "Unhandled predicate: #{action} for #{self}.#{__method__}"
end
end
# @note [@jeremyf] I am re-implemnenting the initialize method, but removing the Pundit
# authorization. There's an assumption that all policy questions will require a user,
# unless you know specifically that they don't.
#
# @note as a reminder, if you attempt to authorize this policy in a controller with that calls
# {CachingHeaders#set_cache_control_headers} you may encounter some headaches. What do
# those headaches look like? When you call {CachingHeaders#set_cache_control_headers}, you
# are likely disallowing checks on current_user (via {EdgeCacheSafetyCheck#current_user}).
#
# @todo [@jeremyf] I don't like altering the initializer and its core assumption. But the other
# option to get Articles working for https://github.com/forem/forem/issues/16529 is to
# address the at present fundamental assumption regarding "Policies are for authorizing when
# you have a user, otherwise let the controller decide."
#
# rubocop:disable Lint/MissingSuper
#
# @see even Rubocop thinks this is a bad idea. But the short-cut gets me unstuck. I hope there's
# enough breadcrumbs to undo this short-cut.
def initialize(user, record)
@user = user
@record = record
end
# rubocop:enable Lint/MissingSuper
def feed?
true
end
# Does the user already have existing articles? Can they create an article?
#
# @return [TrueClass] They have existing published articles OR can create new ones.
# @return [FalseClass] They do not have published articles NOR can they create new ones.
#
# @note As part of our aspirations to only show users what is relevant to them and "hiding" what
# is not, this method will help us with the edge case of "should we show the user a
# dashboard listing of posts?"
#
# @note This handles the case in which a user has lost the ability to create posts (e.g. we've
# toggled on the feature limiting posts to admins only) but they have at least one published
# post. In that case we want to show them things like "their posts's analytics" or a
# dashboard of their published posts.
#
# @note This policy method is a bit different. It is strictly meant to return true or false.
# Other policies might raise exceptions, but the purpose of this method is for conditional
# rendering.
def has_existing_articles_or_can_create_new_ones?
require_user!
return true if user.articles.published.exists?
create?
rescue ApplicationPolicy::NotAuthorizedError
false
end
# @see {ArticlePolicy.scope_users_authorized_to_action} for "mirrored" details.
def create?
require_user_in_good_standing!
return true unless self.class.limit_post_creation_to_admins?
user_any_admin?
end
def update?
require_user_in_good_standing!
user_author? || user_super_admin? || user_org_admin? || user_any_admin?
end
def manage?
update? && record.published? && !record.scheduled?
end
def stats?
require_user!
user_author? || user_super_admin? || user_org_admin?
end
def subscriptions?
require_user!
user_author? || user_super_admin?
end
def elevated_user?
user_any_admin? || user_super_moderator?
end
# this method performs the same checks that determine:
# if the record can be featured
# if user can adjust any tag
# if user can perform moderator actions
def revoke_publication?
require_user!
return false unless @record.published?
elevated_user?
end
def allow_tag_adjustment?
require_user!
elevated_user? || tag_moderator_eligible?
end
def tag_moderator_eligible?
tag_ids_moderated_by_user = Tag.with_role(:tag_moderator, @user).ids
return false unless tag_ids_moderated_by_user.size.positive?
adjustments = TagAdjustment.where(article_id: @record.id)
has_room_for_tags = @record.tag_list.size < MAX_TAG_LIST_SIZE
# ensures that mods cannot adjust an already-adjusted tag
# "zero?" because intersection has just one integer (0 or 1)
has_no_relevant_adjustments = adjustments.pluck(:tag_id).intersection(tag_ids_moderated_by_user).size.zero?
# tag_mod can add their moderated tags
return true if has_room_for_tags && has_no_relevant_adjustments
authorized_to_adjust = @record.tags.ids.intersection(tag_ids_moderated_by_user).size.positive?
# tag_mod can remove their moderated tags
!has_room_for_tags && has_no_relevant_adjustments && authorized_to_adjust
end
def destroy?
require_user!
user_author? || user_super_admin? || user_org_admin? || user_any_admin?
end
def moderate?
# Technically, we could check the limit_post_creation_to_admins? first, but [@jeremyf]'s
# operating on a "trying to maintain consistency" approach.
require_user_in_good_standing!
return false if self.class.limit_post_creation_to_admins?
# <2022-05-09 Mon> Don't let a user moderate their own article; though this may not be the desired behavior.
return false if user_author?
# Beware a trusted user does not guarantee that they are an admin. And more specifically, being
# an admin does not guarantee being trusted.
return true if user.trusted?
elevated_user?
end
alias admin_featured_toggle? revoke_publication?
alias toggle_featured_status? revoke_publication?
alias can_adjust_any_tag? revoke_publication?
alias can_perform_moderator_actions? revoke_publication?
# Due to the associated controller method "admin_unpublish", we
# alias "admin_ubpublish" to the "revoke_publication" method.
alias admin_unpublish? revoke_publication?
alias new? create?
alias delete_confirm? destroy?
alias discussion_lock_confirm? destroy?
alias discussion_unlock_confirm? destroy?
alias edit? update?
# The ArticlesController#preview method is very complicated but aspirationally, we want to ensure
# that someone can preview an article of their if they already have a published article or they
# can create new ones.
alias preview? has_existing_articles_or_can_create_new_ones?
def permitted_attributes
%i[title body_html body_markdown main_image published canonical_url
description tag_list publish_under_org
video video_code video_source_url video_thumbnail_url receive_notifications
archived]
end
private
def user_author?
# We might have the Article class (instead of the Article instance), so let's short circuit
return false unless record.respond_to?(:user_id)
record.user_id == user.id
end
def user_org_admin?
user.org_admin?(record.organization_id)
end
end