opf/openproject

View on GitHub
app/services/authorization/user_permissible_service.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Authorization
  class UserPermissibleService
    attr_accessor :user

    def initialize(user)
      @user = user
    end

    def allowed_globally?(permission)
      perms = contextual_permissions(permission, :global)
      return false unless authorizable_user?
      return true if admin_and_all_granted_to_admin?(perms)

      cached_permissions(nil).intersect?(perms.map(&:name))
    end

    def allowed_in_project?(permission, projects_to_check)
      perms = contextual_permissions(permission, :project)
      return false if projects_to_check.blank?
      return false unless authorizable_user?

      projects = Array(projects_to_check)

      projects.all? do |project|
        allowed_in_single_project?(perms, project)
      end
    end

    def allowed_in_any_project?(permission)
      perms = contextual_permissions(permission, :project)
      return false unless authorizable_user?
      return true if admin_and_all_granted_to_admin?(perms)

      cached_in_any_project?(perms)
    end

    def allowed_in_entity?(permission, entities_to_check, entity_class)
      return false if entities_to_check.blank?
      return false unless authorizable_user?

      perms = contextual_permissions(permission, context_name(entity_class))

      entities = Array(entities_to_check)

      entities.all? do |entity|
        allowed_in_single_entity?(perms, entity)
      end
    end

    def allowed_in_any_entity?(permission, entity_class, in_project: nil) # rubocop:disable Metrics/PerceivedComplexity
      perms = contextual_permissions(permission, context_name(entity_class))
      return false unless authorizable_user?
      return false if in_project && !(in_project.active? || in_project.being_archived?)
      return true if admin_and_all_granted_to_admin?(perms)

      # entity_class.allowed_to will also check whether the user has the permission via a membership in the project.
      # ^-- still a problem in some cases
      allowed_scope = entity_class.allowed_to(user, perms)

      if in_project
        allowed_in_single_project?(perms, in_project) || allowed_scope.exists?(project: in_project)
      else
        allowed_in_any_project?(perms) || allowed_scope.exists?
      end
    end

    private

    def cached_permissions(context)
      @cached_permissions ||= Hash.new do |hash, context_key|
        hash[context_key] = user.all_permissions_for(context_key)
      end

      @cached_permissions[context]
    end

    def cached_in_any_project?(permissions)
      @any_project_cache ||= Hash.new do |hash, perm|
        hash[perm] = Project.allowed_to(user, perm).exists?
      end

      permissions.any? { |permission| @any_project_cache[permission] }
    end

    def allowed_in_single_project?(permissions, project)
      return false if project.nil?
      return false unless project.active? || project.being_archived?

      permissions_filtered_for_project = permissions_by_enabled_project_modules(project, permissions)

      return false if permissions_filtered_for_project.empty?
      return true if admin_and_all_granted_to_admin?(permissions)

      cached_permissions(project).intersect?(permissions_filtered_for_project)
    end

    def allowed_in_single_entity?(permissions, entity)
      return false if entity.nil?
      return false if entity.project.nil?
      return false unless entity.project.active? || entity.project.being_archived?

      permissions_filtered_for_project = permissions_by_enabled_project_modules(entity.project, permissions)

      return false if permissions_filtered_for_project.empty?
      return true if admin_and_all_granted_to_admin?(permissions)

      # The combination of this is better then doing
      # EntityClass.allowed_to(user, permission).exists?.
      # Because this way, all permissions for that context are fetched and cached.
      allowed_in_single_project?(permissions, entity.project) ||
        cached_permissions(entity).intersect?(permissions_filtered_for_project)
    end

    def admin_and_all_granted_to_admin?(permissions)
      user.admin? && permissions.all?(&:grant_to_admin?)
    end

    def authorizable_user?
      !user.locked? || user.is_a?(SystemUser)
    end

    def permissions_by_enabled_project_modules(project, permissions)
      project
        .allowed_permissions
        .intersection(permissions.map(&:name))
        .map { |perm| perm.name.to_sym }
    end

    def contextual_permissions(permission, context)
      Authorization.contextual_permissions(permission, context, raise_on_unknown: true)
    end

    def context_name(entity_class)
      entity_class.model_name.element.to_sym
    end
  end
end