opf/openproject

View on GitHub
app/models/users/permission_checks.rb

Summary

Maintainability
A
0 mins
Test Coverage
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Users::PermissionChecks
  extend ActiveSupport::Concern

  included do
    # Some Ruby magic. Create methods for each entity we can have memberships on automatically
    # i.e. allowed_in_work_package? and allowed_in_any_work_package?
    Member::ALLOWED_ENTITIES.each do |entity_model_name|
      entity_name_underscored = entity_model_name.underscore
      entity_class = entity_model_name.constantize

      define_method :"allowed_in_#{entity_name_underscored}?" do |permission, entity|
        allowed_in_entity?(permission, entity, entity_class)
      end

      define_method :"allowed_in_any_#{entity_name_underscored}?" do |permission, in_project: nil|
        allowed_in_any_entity?(permission, entity_class, in_project:)
      end
    end
  end

  class_methods do
    def allowed(action, project)
      Authorization.users(action, project)
    end

    def allowed_members(action, project)
      Authorization.users(action, project).where.not(members: { id: nil })
    end

    def allowed_members_on_work_package(action, work_package)
      project_members = allowed_members(action, work_package.project)
                          .where(members: { entity: nil })
      work_package_members = allowed_members(action, work_package.project)
                               .where(members: { entity: work_package })

      project_members.or(work_package_members)
    end
  end

  def reload(*args)
    @user_permissible_service = nil
    @user_allowed_service = nil
    @project_role_cache = nil

    super
  end

  # All the new methods to check for permissions. This will completely replace the old interface:
  delegate :allowed_globally?,
           :allowed_in_project?,
           :allowed_in_any_project?,
           :allowed_in_entity?,
           :allowed_in_any_entity?,
           to: :user_permissible_service

  # Return user's roles for project
  def roles_for_project(project)
    project_role_cache.fetch(project)
  end
  alias :roles :roles_for_project

  # Return user's role for the work package.
  # Which consists of both the roles granted to the user directly on the work package
  # as well as those granted to the user on the project the work package belongs to.
  def roles_for_work_package(work_package)
    roles_for_project(work_package.project) +
      Role.includes(:member_roles)
          .where(member_roles: { member_id: Member.of_work_package(work_package).select(:id) })
  end

  # Return true when the user is either a member of the project or any resource under the project
  def access_to?(project)
    admin? || members.exists?(project_id: project.id)
  end

  # Return true if the user is a member of project
  def member_of?(project)
    roles_for_project(project).any?(&:member?)
  end

  def all_permissions_for(context)
    Authorization
      .roles(self, context)
      .includes(:role_permissions)
      .pluck(:permission)
      .compact
      .map(&:to_sym)
      .uniq
  end

  # Helper method to be used in places where we just throw anything into the permission check and don't know what
  # context it should be checked on. Things like menu item checks, controller action checks, generic services, etc.
  def allowed_based_on_permission_context?(permission, project: nil, entity: nil) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
    permissions = Authorization.permissions_for(permission, raise_on_unknown: true)

    entity_blank_or_not_project_scoped = entity.blank? || !entity.respond_to?(:project) || (entity.respond_to?(:project) && entity.project.blank?)
    entity_is_work_package_or_list = (entity.is_a?(WorkPackage) && entity.persisted?) || (entity.is_a?(Enumerable) && entity.all?(WorkPackage))
    entity_is_project_scoped_and_project_is_present = entity.respond_to?(:project) && entity.project.present?

    permissions.any? do |perm|
      if perm.global?
        allowed_globally?(perm)
      elsif perm.work_package? && entity_is_work_package_or_list
        allowed_in_work_package?(perm, entity)
      elsif perm.work_package? && entity.blank? && project.blank?
        allowed_in_any_work_package?(perm)
      elsif perm.work_package? && entity && entity.new_record? && entity.respond_to?(:project)
        allowed_in_any_work_package?(perm, in_project: entity.project)
      elsif perm.work_package? && project && (entity.blank? || entity.new_record?)
        allowed_in_any_work_package?(perm, in_project: project)
      elsif perm.project? && project
        allowed_in_project?(perm, project)
      elsif perm.project? && project.nil? && entity.present? && entity_is_project_scoped_and_project_is_present
        allowed_in_project?(perm, entity.project)
      elsif perm.project? && entity_blank_or_not_project_scoped && project.blank?
        allowed_in_any_project?(perm)
      else
        false
      end
    end
  end

  private

  def user_permissible_service
    @user_permissible_service ||= ::Authorization::UserPermissibleService.new(self)
  end

  def project_role_cache
    @project_role_cache ||= ::Users::ProjectRoleCache.new(self)
  end
end