QutBioacoustics/baw-server

View on GitHub
app/modules/access/core.rb

Summary

Maintainability
C
1 day
Test Coverage
# frozen_string_literal: true

# A module for applying access restrictions.
module Access
  # Basic level, user, and access methods.
  class Core
    class << self
      # Get a hash with symbols, names, action words for the available levels.
      # @return [Hash]
      def levels_hash
        {
          owner: {
            name: 'Owner',
            action: 'own'
          },
          writer: {
            name: 'Writer',
            action: 'write'
          },
          reader: {
            name: 'Reader',
            action: 'read'
          }
        }
      end

      # Get all the valid levels.
      # @return [Array<Symbol>]
      def levels
        Access::Core.levels_hash.keys
      end

      # Get value indicating no access level.
      # @return [nil]
      def levels_none
        nil
      end

      # Get a hash with symbols, names, action words for the available roles.
      # @return [Hash]
      def roles_hash
        {
          admin: {
            name: 'Administrator',
            action: 'administer'
          },
          user: {
            name: 'User',
            action: 'use'
          },
          harvester: {
            name: 'Harvester',
            action: 'harvest'
          },
          guest: {
            name: 'Guest',
            action: 'use as guest'
          }
        }
      end

      # Get all the valid roles.
      # @return [Array<Symbol>]
      def roles
        User.valid_roles
      end

      # Get level display name.
      # @param [Symbol] key
      # @return [String] name
      def get_level_name(key)
        key = Access::Validate.level(key)
        get_hash_value(Access::Core.levels_hash, key, :name)
      end

      # Get level action word.
      # @param [Symbol] key
      # @return [String] action word
      def get_level_action(key)
        key = Access::Validate.level(key)
        get_hash_value(Access::Core.levels_hash, key, :action)
      end

      # Get role display name.
      # @param [Symbol] key
      # @return [String] name
      def get_role_name(key)
        key = Access::Validate.role(key)
        get_hash_value(Access::Core.roles_hash, key, :name)
      end

      # Get role action word.
      # @param [Symbol] key
      # @return [String] action word
      def get_role_action(key)
        key = Access::Validate.role(key)
        get_hash_value(Access::Core.roles_hash, key, :action)
      end

      # Get value from hash of symbols, names, and action words.
      # @param [Hash] hash
      # @param [Symbol] key
      # @param [Symbol] attribute
      # @return [String] value
      def get_hash_value(hash, key, attribute)
        hash[key][attribute]
      end

      # Normalize a level identifier.
      # @param [Object] level
      # @return [Symbol, nil] normalized level or nil
      def normalise_level(level)
        return nil if level.blank?

        case level.to_s
        when 'reader', 'read'
          :reader
        when 'writer', 'write'
          :writer
        when 'owner', 'own'
          :owner
        end
      end
      alias normalize_level normalise_level

      # Validate access level.
      # @param [Object] level
      # @return [Symbol] level
      def validate_level(level)
        raise ArgumentError, 'Access level must not be blank.' if level.blank?

        valid_levels = levels.keys
        level_sym = level.to_sym

        unless valid_levels.include?(level_sym)
          raise ArgumentError, "Access level '#{level_sym}' is not in available levels '#{valid_levels}'."
        end

        level_sym
      end

      # Validate array of access levels.
      # @param [Array<Symbol>] levels
      # @return [Array<Symbol>] levels
      def validate_levels(levels)
        raise ArgumentError, 'Access level array must not be blank.' if levels.blank?

        levels = [levels] unless levels.respond_to?(:map)

        levels_sym = levels.map { |i| validate_level(i) }.uniq
        validate_level_combination(levels_sym)
        levels_sym
      end

      # Validate level combination.
      # @param [Array<Symbol>] levels
      # @return [Array<Symbol>] levels
      def validate_level_combination(levels)
        if levels.respond_to?(:each)
          if (levels.include?(:none) || levels.include?('none')) && levels.size > 1
            # none cannot be with other levels because this can be ambiguous, and points to a problem with how the
            # permissions were obtained.
            raise ArgumentError, "Level array cannot contain none with other levels, got '#{levels.join(', ')}'."
          else
            levels
          end
        else
          raise ArgumentError, "Must be an array of levels, got '#{levels.class}'."
        end
      end

      # Validate Project.
      # @param [Project] project
      # @return [Project] project
      def validate_project(project)
        if project.blank? || !project.is_a?(Project)
          raise ArgumentError, "Project was not valid, got '#{project.class}'."
        end

        project
      end

      # Validate Projects.
      # @param [Array<Project>] projects
      # @return [Array<Project>] projects
      def validate_projects(projects)
        raise ArgumentError, 'No projects provided.' if projects.blank?

        projects.to_a.map { |p| validate_project(p) }
      end

      # Validate User. User can be nil.
      # @param [User] user
      # @return [User] user
      def validate_user(user)
        raise ArgumentError, "User was not valid, got '#{user.class}'." if !user.blank? && !user.is_a?(User)

        user
      end

      # Validate Users. User can be nil.
      # @param [Array<User>] users
      # @return [Array<User>] users
      def validate_users(users)
        users.to_a.map { |u| validate_user(u) }
      end

      # Validate audio recording.
      # @param [AudioRecording] audio_recording
      # @return [AudioRecording] audio_recording
      def validate_audio_recording(audio_recording)
        if audio_recording.blank? || !audio_recording.is_a?(AudioRecording)
          raise ArgumentError, "AudioRecording was not valid, got '#{audio_recording.class}'."
        end

        audio_recording
      end

      # Validate audio recordings.
      # @param [Array<AudioRecording>] audio_recordings
      # @return [Array<AudioRecording>] audio_recordings
      def validate_audio_recordings(audio_recordings)
        audio_recordings.to_a.map { |u| validate_audio_recording(u) }
      end

      # Validate audio event.
      # @param [AudioEvent] audio_event
      # @return [AudioEvent] audio_event
      def validate_audio_event(audio_event)
        if audio_event.blank? || !audio_event.is_a?(AudioEvent)
          raise ArgumentError, "AudioRecording was not valid, got '#{audio_event.class}'."
        end

        audio_event
      end

      # Validate audio events.
      # @param [Array<AudioEvent>] audio_events
      # @return [Array<AudioEvent>] audio_events
      def validate_audio_events(audio_events)
        audio_events.to_a.map { |u| validate_audio_event(u) }
      end

      # Validate analysis job.
      # @param [Array<AnalysisJob>] analysis_job
      # @return [Array<AnalysisJob>] analysis_job
      def validate_analysis_job(analysis_job)
        if analysis_job.blank? || !analysis_job.is_a?(AnalysisJob)
          raise ArgumentError, "AnalysisJob was not valid, got '#{analysis_job.class}'."
        end

        analysis_job
      end

      # Validate dataset.
      # @param [Array<Dataset>] dataset
      # @return [Array<Dataset>] dataset
      def validate_dataset(dataset)
        if dataset.blank? || !dataset.is_a?(Dataset)
          raise ArgumentError, "Dataset was not valid, got '#{dataset.class}'."
        end

        dataset
      end

      # Get an array of access levels that are equal or lower.
      # @param [Symbol] level
      # @return [Array<Symbol>]
      def equal_or_lower(level)
        level_sym = Access::Validate.level(level)
        case level_sym
        when :owner
          [:reader, :writer, :owner]
        when :writer
          [:reader, :writer]
        when :reader
          [:reader]
        else
          raise ArgumentError,
            "Can not get equal or lower level for '#{level}', must be one of #{Access::Core.levels.map(&:to_s).join(', ')}."
        end
      end

      # Get an array of access levels that are equal or greater.
      # @param [Symbol] level
      # @return [Array<Symbol>]
      def equal_or_greater(level)
        level_sym = Access::Validate.level(level)
        case level_sym
        when :owner
          [:owner]
        when :writer
          [:writer, :owner]
        when :reader
          [:reader, :writer, :owner]
        else
          raise ArgumentError,
            "Can not get equal or greater level for '#{level}', must be one of #{Access::Core.levels.map(&:to_s).join(', ')}."
        end
      end

      # Get the highest access level.
      # @param [Array<Symbol>] levels
      # @return [Symbol]
      def highest(levels)
        levels_sym = Access::Validate.levels(levels)

        return :owner if levels_sym.include?(:owner)
        return :writer if levels_sym.include?(:writer)
        return :reader if levels_sym.include?(:reader)

        nil
      end

      # Get the lowest access level.
      # @param [Array<Symbol>] levels
      # @return [Symbol]
      def lowest(levels)
        levels_sym = Access::Validate.levels(levels)

        return nil if Access::Core.is_no_level?(levels)
        return :reader if levels_sym.include?(:reader)
        return :writer if levels_sym.include?(:writer)

        :owner if levels_sym.include?(:owner)
      end

      # Check if these levels equate to no access level.
      # @param [Object] levels
      # @return [Boolean] true if is no access level, otherwise false
      def is_no_level?(levels)
        levels = Access::Validate.levels(levels)
        level = highest(levels)
        level.nil?
      end

      # Is this user an admin?
      # An admin is a user with the :admin role.
      # @param [User] user
      # @return [Boolean]
      def is_admin?(user)
        return false if Access::Core.is_guest?(user)

        user.has_role?(:admin)
      end

      # Is this user a guest?
      # A guest is a nil user or a user with the :guest role or a user without an id.
      # An unconfirmed user is not a guest user.
      # e.g. in some cases, current_user will be blank
      # e.g. for ability.rb, a user is created with role set as guest
      # @param [User] user
      # @return [Boolean]
      def is_guest?(user)
        Access::Validate.user(user)
        user.blank? || user.has_role?(:guest) || user.id.nil?
      end

      # Is this user an unconfirmed user?
      # An unconfirmed user has the :user role and has not confirmed their email.
      # NOT CURRENTLY USED.
      # @param [User] user
      # @return [Boolean]
      def is_unconfirmed_user?(user)
        return false if Access::Core.is_guest?(user)

        !user.confirmed?
      end

      # Is this user a standard user?
      # A standard user has the :user role.
      # @param [User] user
      # @return [Boolean]
      def is_standard_user?(user)
        return false if Access::Core.is_guest?(user)

        user.has_role?(:user)
      end

      # Is this user a harvester user?
      # A harvester user has the :harvester role.
      # @param [User] user
      # @return [Boolean]
      def is_harvester?(user)
        return false if Access::Core.is_guest?(user)

        user.has_role?(:harvester)
      end

      # Check if requested access level(s) is allowed based on actual access level(s).
      # If actual is a higher level than requested, it is allowed.
      # @param [Symbol, Array<Symbol>] requested_levels
      # @param [Symbol, Array<Symbol>] actual_levels
      # @return [Boolean]
      def allowed?(required_level, actual_levels)
        requested_array = Access::Validate.levels([required_level])
        actual_array = Access::Validate.levels([actual_levels])

        # short circuit checking nils
        return false if requested_array.blank? || requested_array.compact.blank? ||
                        actual_array.blank? || actual_array.compact.blank?

        actual_highest = Access::Core.highest(actual_array)
        actual_equal_or_lower = Access::Core.equal_or_lower(actual_highest)
        requested_highest = Access::Core.highest(requested_array)

        actual_equal_or_lower.include?(requested_highest)
      end

      # Does this user have this access level to this project?
      # @param [User] user
      # @param [Symbol] level
      # @param [Project] project
      # @return [Boolean]
      def can?(user, level, project)
        can_any?(user, level, [project])
      end

      # Does this user not have any access to this project?
      # @param [User] user
      # @param [Project] project
      # @return [Boolean]
      def cannot?(user, project)
        !can?(user, :reader, project)
      end

      # Does this user have this access level to any of these projects?
      # @param [User] user
      # @param [Symbol,Array<Symbol>] levels one or more required levels. The highest is chosen as the restriction.
      # @param [Array<Project>] projects
      # @return [Boolean]
      def can_any?(user, levels, projects)
        requested_level = Access::Validate.levels(levels)
        actual_level = Access::Core.user_levels(user, projects)

        allowed?(requested_level, actual_level)
      end

      # Does this user not have any access to any of these projects?
      # @param [User] user
      # @param [Array<Project>] projects
      # @return [Boolean]
      def cannot_any?(user, projects)
        !can_any?(user, :reader, projects)
      end

      # Does this user have this access level to all of these projects?
      # @param [User] user
      # @param [Symbol] level
      # @param [Array<Project>] projects
      # @return [Boolean]
      def can_all?(user, level, projects)
        requested_level = Access::Validate.level(level)
        actual_levels = Access::Core.user_levels(user, projects)
        actual_level_lowest = Access::Core.lowest(actual_levels)

        allowed?(requested_level, actual_level_lowest)
      end

      # Does this user not have any access level to all of these projects?
      # @param [User] user
      # @param [Array<Project>] projects
      # @return [Boolean]
      def cannot_all?(user, projects)
        !can_all?(user, :reader, projects)
      end

      # Fail if the site is not in any projects.
      # @param [Site] site
      # @return [void]
      def check_orphan_site!(site)
        return if site.nil?

        if site.projects.empty?
          raise CustomErrors::OrphanedSiteError, "Site #{site.name} (#{site.id}) is not in any projects."
        end
      end

      # Get the access levels for this user to the project(s).
      # This method returns the access levels reflected in the permissions table,
      # so an admin may not have access to every project.
      # @param [User] user
      # @param [Project, Array<Project>] projects
      # @return [Array<Symbol, nil>]
      def user_levels(user, projects)
        # moved this case forward to shortcut project query execution
        return Access::Validate.levels([:owner]) if Access::Core.is_admin?(user)

        projects = Access::Validate.projects([projects])

        # always restricted to specified project(s)
        levels = Permission.where(project_id: projects)

        if Access::Core.is_guest?(user)
          # a guest user's permissions are only specified by :allow_logged_in
          levels = levels.where(user: nil, allow_logged_in: false, allow_anonymous: true)
        elsif !user.blank?
          # a logged in user can have their own permissions or
          # permissions specified by :allow_logged_in
          levels = levels.where('user_id = ? OR allow_logged_in IS TRUE', user.id)
        else
          raise ArgumentError, "Invalid user to retrieve levels: '#{user}'."
        end

        levels = levels.pluck(:level)
        Access::Validate.levels(levels)
      end

      # Get the access levels for this user to the project(s).
      # This method returns only individual user access levels reflected in the permissions table,
      # so an admin may not have access to every project.
      # @param [User] user
      # @param [Project, Array<Project>] projects
      # @return [Array<Symbol, nil>]
      def user_only_levels(user, projects)
        projects = Access::Validate.projects([projects])
        raise ArgumentError, 'Must provide a user, nil is not valid.' if Access::Core.is_guest?(user)

        levels = Permission
                 .where(project_id: projects)
                 .where('user_id = ?', user.id)
                 .pluck(:level)

        Access::Validate.levels(levels)
      end

      # Get the anonymous user access levels for this user to the project(s).
      # @param [Project, Array<Project>] projects
      # @return [Array<Symbol, nil>]
      def anon_levels(projects)
        user_levels(nil, projects)
      end

      # Get the access levels for logged in users to the project(s).
      # @param [Project, Array<Project>] projects
      # @return [Array<Symbol, nil>]
      def logged_in_levels(projects)
        projects = Access::Validate.projects([projects])

        levels = Permission
                 .where(project_id: projects)
                 .where('allow_logged_in IS TRUE')
                 .pluck(:level)

        Access::Validate.levels(levels)
      end
    end
  end
end