ManageIQ/manageiq

View on GitHub
app/models/authenticator/base.rb

Summary

Maintainability
B
4 hrs
Test Coverage
A
90%
module Authenticator
  class Base
    include Vmdb::Logging

    def self.validate_config(_config)
      []
    end

    def self.authenticates_for
      [name.demodulize.underscore]
    end

    def self.authorize(config, *args)
      new(config).authorize(*args)
    end

    def self.short_name
      name.demodulize.underscore
    end

    attr_reader :config

    def initialize(config)
      @config = config
    end

    def validate_config
      self.class.validate_config(config)
    end

    def uses_stored_password?
      false
    end

    def user_authorizable_without_authentication?
      false
    end

    def user_authorizable_with_system_token?
      false
    end

    def authorize_user(userid)
      return unless user_authorizable_without_authentication?

      authenticate(userid, "", {}, {:require_user => true, :authorize_only => true})
    end

    def authorize_user_with_system_token(userid, user_metadata = {})
      return unless user_authorizable_without_authentication? && user_authorizable_with_system_token?
      return if userid != user_metadata[:userid]

      authenticate(userid, "", {}, {:require_user => true, :authorize_only => true, :authorize_with_system_token => user_metadata})
    end

    def authenticate(username, password, request = nil, options = {})
      log_auth_debug("authenticate(username=#{username}, options=#{options})")

      options = options.dup
      options[:require_user] ||= false
      options[:authorize_only] ||= false

      user_or_taskid = nil

      begin
        username = normalize_username(username)
        audit = {:event => audit_event, :userid => username}

        # The fail_message might or might not come from the _authenticate method
        authenticated, fail_message = options[:authorize_only] || _authenticate(username, password, request)
        fail_message ||= _("Authentication failed") # Fall back to the default fail_message

        if authenticated
          audit_success(audit.merge(:message => "User #{username} successfully validated by #{self.class.proper_name}"))

          if authorize?
            user_or_taskid = authorize_queue(username, request, options)
          else
            # If role_mode == database we will only use the external system for authentication. Also, the user must exist in our database
            # otherwise we will fail authentication
            user_or_taskid = lookup_by_identity(username, request)
            user_or_taskid ||= autocreate_user(username)

            unless user_or_taskid
              audit_failure(audit.merge(:message => "User #{username} authenticated but not defined in EVM"))
              raise MiqException::MiqEVMLoginError,
                    _("User authenticated but not defined in EVM, please contact your EVM administrator")
            end
          end

          audit_success(audit.merge(:message => "Authentication successful for user #{username}"))
        else
          reason = failure_reason(username, request)
          reason = ": #{reason}" if reason.present?
          audit_failure(audit.merge(:message => "Authentication failed for userid #{username}#{reason}"))
          raise MiqException::MiqEVMLoginError, fail_message
        end
      rescue MiqException::MiqEVMLoginError => err
        _log.warn(err.message)
        raise
      rescue Exception => err
        _log.log_backtrace(err)
        raise MiqException::MiqEVMLoginError, err.message
      end

      if options[:require_user] && !user_or_taskid.kind_of?(User)
        task = MiqTask.wait_for_taskid(user_or_taskid, options)
        if task.nil? || MiqTask.status_error?(task.status) || MiqTask.status_timeout?(task.status)
          raise MiqException::MiqEVMLoginError, fail_message
        end

        user_or_taskid = case_insensitive_find_by_userid(task.userid)
      end

      if user_or_taskid.kind_of?(User)
        user_or_taskid.lastlogon = Time.now.utc
        user_or_taskid.save!
      end

      user_or_taskid
    end

    def authorize(taskid, username, *args)
      audit = {:event => "authorize", :userid => username}
      decrypt_ldap_password(config) if MiqLdap.using_ldap?

      run_task(taskid, "Authorizing") do |task|
        begin
          identity = find_external_identity(username, args[0], args[1])

          unless identity
            msg = "Authentication failed for userid #{username}, unable to find user object in #{self.class.proper_name}"
            _log.warn(msg)
            audit_failure(audit.merge(:message => msg))
            task.error(msg)
            task.state_finished
            return nil
          end

          incoming_groups = groups_for(identity)
          matching_groups = match_groups(incoming_groups)
          userid, user = find_or_initialize_user(identity, username)
          update_user_attributes(user, userid, identity)
          audit_new_user(audit, user) if user.new_record?
          user.miq_groups = matching_groups

          if matching_groups.empty?
            msg = "Authentication failed for userid #{user.userid}, unable to match user's group membership to an EVM role. The incoming groups are: #{incoming_groups.join(", ")}"
            _log.warn(msg)
            audit_failure(audit.merge(:message => msg))
            task.error(msg)
            task.state_finished
            user.save! unless user.new_record?
            return nil
          end

          user.lastlogon = Time.now.utc
          if user.new_record?
            User.with_lock do
              user.save!
            rescue ActiveRecord::RecordInvalid # Try update when catching create race condition.
              userid, user = find_or_initialize_user(identity, username)
              update_user_attributes(user, userid, identity)
              user.miq_groups = matching_groups
              user.save!
            end
          else
            user.save!
          end

          _log.info("Authorized User: [#{user.userid}]")
          task.userid = user.userid
          task.update_status("Finished", "Ok", "User authorized successfully")

          user
        rescue Exception => err
          audit_failure(audit.merge(:message => err.message))
          raise
        end
      end
    end

    def find_external_identity(_username, _user_attrs, _membership_list)
      raise NotImplementedError, _("find_external_identity must be implemented in a subclass")
    end

    def find_or_initialize_user(identity, username)
      userid = userid_for(identity, username)
      user   = case_insensitive_find_by_userid(userid)
      user ||= User.new(:userid => userid)
      [userid, user]
    end

    def authenticate_with_http_basic(username, password, request = nil, options = {})
      options[:require_user] ||= false
      user, username = lookup_by_principalname(username)
      result = nil
      begin
        result = user && authenticate(username, password, request, options)
      rescue MiqException::MiqEVMLoginError
      end
      audit_failure(:userid => username, :message => "Authentication failed for user #{username}") if result.nil?
      [!!result, username]
    end

    def lookup_by_identity(username, *_args)
      case_insensitive_find_by_userid(username)
    end

    # FIXME: LDAP
    def lookup_by_principalname(username)
      unless (user = case_insensitive_find_by_userid(username))
        if username.include?('\\')
          parts = username.split('\\')
          username = "#{parts.last}@#{parts.first}"
        elsif !username.include?('@') && MiqLdap.using_ldap?
          suffix = config[:user_suffix]
          username = "#{username}@#{suffix}"
        end
        user = case_insensitive_find_by_userid(username)
      end
      [user, username]
    end

    alias find_by_principalname lookup_by_principalname
    Vmdb::Deprecation.deprecate_methods(self, :find_by_principalname => :lookup_by_principalname)

    private

    def audit_event
      "authenticate_#{self.class.short_name}"
    end

    def audit_new_user(audit, user)
      msg = "User creation successful for User: #{user.name} with ID: #{user.userid}"
      audit_success(audit.merge(:message => msg))
      MiqEvent.raise_evm_event_queue(MiqServer.my_server, "user_created", :event_details => msg)
    end

    def authorize?
      config[:"#{self.class.short_name}_role"] == true
    end

    def failure_reason(_username, _request)
      nil
    end

    def case_insensitive_find_by_userid(username)
      user =  User.lookup_by_userid(username)
      user || User.in_my_region.where(:lower_userid => username.downcase).order(:lastlogon).last
    end

    def userid_for(_identity, username)
      username
    end

    def authorize_queue?
      !defined?(Rails::Server)
    end

    def decrypt_ldap_password(config)
      config[:bind_pwd] = ManageIQ::Password.try_decrypt(config[:bind_pwd])
    end

    def encrypt_ldap_password(config)
      config[:bind_pwd] = ManageIQ::Password.try_encrypt(config[:bind_pwd])
    end

    def authorize_queue(username, _request, _options, *args)
      task = MiqTask.create(:name => "#{self.class.proper_name} User Authorization of '#{username}'", :userid => username)
      if authorize_queue?
        encrypt_ldap_password(config) if MiqLdap.using_ldap?
        MiqQueue.submit_job(
          :class_name   => self.class.to_s,
          :method_name  => "authorize",
          :args         => [config, task.id, username, *args],
          :server_guid  => MiqServer.my_guid,
          :priority     => MiqQueue::HIGH_PRIORITY,
          :miq_callback => {
            :class_name  => task.class.name,
            :instance_id => task.id,
            :method_name => :queue_callback_on_exceptions,
            :args        => ['Finished']
          }
        )
      else
        authorize(task.id, username, *args)
      end

      task.id
    end

    def run_task(taskid, status)
      task = MiqTask.find_by(:id => taskid)
      if task.nil?
        message = _("Unable to find task with id: [%{task_id}]") % {:task_id => taskid}
        _log.error(message)
        raise message
      end
      task.update_status("Active", "Ok", status)

      begin
        yield task
      rescue Exception => err
        _log.log_backtrace(err)
        task.error(err.message)
        task.state_finished
        raise
      end
    end

    # TODO: Fix this icky select matching with tenancy
    def match_groups(external_group_names)
      return [] if external_group_names.empty?

      external_group_names = external_group_names.collect(&:downcase)
      internal_groups = MiqGroup.in_my_region.order(:sequence).to_a

      _log.debug("External Groups: #{external_group_names.join(", ")}")
      _log.debug("Internal Groups: #{internal_groups.map { |g| g.description.downcase }.join(", ")}")

      internal_groups.select { |g| external_group_names.include?(g.description.downcase) }
    end

    def autocreate_user(_username)
      nil
    end

    def normalize_username(username)
      (username || '').downcase
    end

    def debug_auth?
      !!Settings.authentication.debug
    end

    def log_auth_debug(msgs)
      Array(msgs).each { |msg| _log.info(msg) } if debug_auth?
    end

    private def audit_success(options)
      AuditEvent.success(options)
    end

    private def audit_failure(options)
      AuditEvent.failure(options)
      MiqEvent.raise_evm_event_queue(MiqServer.my_server, "login_failed", options)
    end
  end
end