docbrown/app/policies/authorizer.rb
Jeremy Friesen a40efc6bbd
Refactoring questions asked of user (#15762)
* Refactoring questions asked of user

In this pull request, I'm extracting and normalizing role-based
questions asked of the user.

Prior to this commit, our codebase has asked two very similar questions
of our user model:

- `user.has_role?(:admin)`
- `user.admin?`

In asking `has_role?(:admin)` we are relying on implementation details
of the rolify gem.  In addition, the `has_role?` question asked
throughout controllers or views means that it's harder to create
hieararchies of permissions.

In favoring `user.admin?` as our question, we can use that indirection
as an opportunity to discuss and decide "Should someone with the
`:super_admin` role be `user.admin? == true`?"

The details of this commit is to do three primary things:

1. Ask the `has_role?` questions in "one place" in the code (e.g. the
   `Authorizer` module)
2. Extract the role based questions that are on the `User` model and
   provde backwards compatable delegation.
3. Structure the code so that it's harder to accidentally call
   `user.has_role?` (e.g., make `User#has_role?` and `User#has_any_role?`
   private).

This is related to #15624 and the updates are informed by discussion in
PR #15691.  This commit supplants #15691.

* Refactoring the liquid tag policy tests

* Fixing typo

* Bump for travis
2021-12-21 12:45:12 -05:00

182 lines
4.8 KiB
Ruby

##
# This module is providing a "crease in the code" for refactoring.
# The initial purpose is to help move away sending `has_role?`
# messages to User records. Prior to this refactor, there were calls
# to `user.has_role?(:tech_admin)` and `user.tech_admin?`; this
# created leaks in abstraction (see
# Authorizer::RoleBasedQueries#admin? for an example).
#
# By moving towards a single entry point and communicating that
# deprecation, the hope is to make the next conversation about roles
# easier.
module Authorizer
# @api private
#
# @note This method introduces some indirection, the idea being that
# the `Authorizer::RoleBasedQueries` is a refactor to convey
# deprecations and provide guidance on sending things through
# a common method pattern (e.g. favor `user.tech_admin?` over
# `user.has_role?(:tech_admin)`).
#
# @param user [User] the user of whom we're curious about their
# attributes and how the imply permissions.
#
def self.for(user:)
RoleBasedQueries.new(user: user)
end
# @api private
#
# This class is responsible for assisting in moving us away from
# `user.some_property_for_permissions?`.
#
# @see https://github.com/forem/forem/issues/15624
class RoleBasedQueries
ANY_ADMIN_ROLES = %i[admin super_admin].freeze
def initialize(user:)
@user = user
end
attr_reader :user
def admin?
has_role?(:admin)
end
def administrative_access_to?(resource:, role_name: :single_resource_admin)
# The implementation details of rolify are such that we can't
# quite combine these functions.
return true if has_any_role?(*ANY_ADMIN_ROLES)
if resource
has_role?(role_name, resource)
else
has_role?(role_name)
end
end
def any_admin?
has_any_role?(*ANY_ADMIN_ROLES)
end
def auditable?
trusted? || tag_moderator? || any_admin?
end
def banished?
user.username.starts_with?("spam_")
end
def comment_suspended?
has_role?(:comment_suspended)
end
def creator?
has_role?(:creator)
end
# When you need to know if we trust the user, but don't want to
# have stale information that the `trusted?` method might give
# you.
#
# @note You may ask why not use the trusted? method on this class?
# Well, in looking at the code there were explicit calls to
# `user.has_role?(:trusted)` which circumvented the caching
# logic. I'm uncertain which of those is appropriate, so
# I'm adding this method here.
#
# @see #trusted?
#
# @todo Review whether we can use trusted? or if we even need to cache things.
def has_trusted_role?
has_role?(:trusted)
end
def podcast_admin_for?(podcast)
has_role?(:podcast_admin, podcast)
end
def single_resource_admin_for?(resource)
has_role?(:single_resource_admin, resource)
end
# @note This is of "narrower" permissions than
# `#user_subscription_tag_available?`, as it doesn't include
# administrators.
#
# @todo Remove this?
def restricted_liquid_tag_for?(liquid_tag)
has_role?(:restricted_liquid_tag, liquid_tag)
end
def super_admin?
has_role?(:super_admin)
end
def support_admin?
has_role?(:support_admin)
end
def suspended?
has_role?(:suspended)
end
def tag_moderator?(tag: nil)
# Note a fan of "peeking" into the roles table, which in a way
# circumvents the rolify gem. But this was the past implementation.
return user.roles.exists?(name: "tag_moderator") unless tag
has_role?(:tag_moderator, tag)
end
def tech_admin?
has_any_role?(:tech_admin, :super_admin)
end
def trusted
ActiveSupport::Deprecation.warn("User#trusted is deprecated, favor User#trusted?")
trusted?
end
def trusted?
return @trusted if defined? @trusted
@trusted = Rails.cache.fetch("user-#{user.id}/has_trusted_role", expires_in: 200.hours) do
has_role?(:trusted)
end
end
def user_subscription_tag_available?
administrative_access_to?(role_name: :restricted_liquid_tag, resource: LiquidTags::UserSubscriptionTag)
end
def vomited_on?
Reaction.exists?(reactable: user, category: "vomit", status: "confirmed")
end
def warned?
has_role?(:warned)
end
def warned
ActiveSupport::Deprecation.warn("User#warned is deprecated, favor User#warned?")
warned?
end
def workshop_eligible?
has_any_role?(:workshop_pass)
end
private
def has_role?(*args)
user.__send__(:has_role?, *args)
end
def has_any_role?(*args)
user.__send__(:has_any_role?, *args)
end
end
private_constant :RoleBasedQueries
end