ManageIQ/manageiq-api

View on GitHub
app/controllers/api/base_controller/authentication.rb

Summary

Maintainability
A
25 mins
Test Coverage
A
90%
module Api
  class BaseController
    module Authentication
      class AuthenticationError < StandardError; end

      SYSTEM_TOKEN_TTL = 30.seconds

      def auth_mechanism
        if request.headers[HttpHeaders::MIQ_TOKEN]
          :system
        elsif request.headers[HttpHeaders::AUTH_TOKEN]
          :token
        elsif request.headers["HTTP_AUTHORIZATION"].try(:match, /^Bearer (.*)/)
          :jwt
        elsif request.headers["HTTP_AUTHORIZATION"]
          # For AJAX requests the basic auth type should be distinguished
          request.headers['X-REQUESTED-WITH'] == 'XMLHttpRequest' ? :basic_async : :basic
        elsif request.cookies['_vmdb_session'] || request.x_csrf_token
          # Even if the session cookie is not set, we want to consider a request
          # as a UI authentication request. (Previously that would force a login popup.)
          :ui_session
        else
          # no attempt at authentication, usually falls back to :basic
          nil
        end
      end

      def authenticate_user
        case auth_mechanism
        when :system
          authenticate_with_system_token(request.headers[HttpHeaders::MIQ_TOKEN])
        when :token
          authenticate_with_user_token(request.headers[HttpHeaders::AUTH_TOKEN])
        when :ui_session
          raise AuthenticationError unless valid_ui_session?
          auth_user(session[:userid])
        when :jwt
          authenticate_with_jwt
        when :basic, :basic_async, nil
          success = authenticate_with_http_basic { |u, p| basic_authentication(u, p) }
          raise AuthenticationError unless success
        end
        log_api_auth
      end

      #
      # REST APIs Authenticator and Redirector
      #
      def require_api_user_or_token
        authenticate_user
      rescue AuthenticationError => e
        api_log_error("AuthenticationError: #{e.message}")
        response.headers["Content-Type"] = "application/json"
        error_message = ErrorSerializer.new(:unauthorized, e).serialize(true).to_json
        render :status => 401, :json => error_message
        log_api_response
      end

      #
      # Attempt auth if desired, but flow through on failure
      #
      def optional_api_user_or_token
        authenticate_user
      rescue AuthenticationError => e
        api_log_warn("AuthenticationError: #{e.message} (on #{request.method} that doesn't require it... ignoring)")
      end

      def user_settings
        {
          :locale                     => I18n.locale.to_s.sub('-', '_'),
          :asynchronous_notifications => ::Settings.server.asynchronous_notifications,
        }.merge(User.current_user.settings)
      end

      def authorize_user_group(user_obj)
        group_name = request.headers[HttpHeaders::MIQ_GROUP]
        if group_name.present?
          group_name = CGI.unescape(group_name)
          group_obj = user_obj.miq_groups.find_by(:description => group_name)
          raise AuthenticationError, "Invalid Authorization Group #{group_name} specified" if group_obj.nil?
          user_obj.current_group_by_description = group_name
        end
      end

      def validate_user_identity(user_obj)
        missing_feature = User.missing_user_features(user_obj)
        if missing_feature
          raise AuthenticationError, "Invalid User #{user_obj.userid} specified, User's #{missing_feature} is missing"
        end
      end

      private

      def clear_cached_current_user
        User.current_user = nil
      end

      def api_token_mgr
        Environment.user_token_service.token_mgr('api')
      end

      def auth_user(userid)
        auth_user_obj = User.lookup_by_identity(userid)
        authorize_user_group(auth_user_obj)
        validate_user_identity(auth_user_obj)
        User.current_user = auth_user_obj
      end

      def authenticate_with_user_token(auth_token)
        if !api_token_mgr.token_valid?(auth_token)
          raise AuthenticationError, "Invalid Authentication Token #{auth_token} specified"
        else
          userid = api_token_mgr.token_get_info(auth_token, :userid)
          raise AuthenticationError, "Invalid Authentication Token #{auth_token} specified" unless userid

          unless request.headers['X-Auth-Skip-Token-Renewal'] == 'true'
            api_token_mgr.reset_token(auth_token)
          end

          auth_user(userid)
        end
      rescue TokenManager::MissingTokenError
        raise AuthenticationError, "Missing Authentication Token (#{HttpHeaders::AUTH_TOKEN})"
      rescue TokenManager::EmptyTokenError
        raise AuthenticationError, "Empty Authentication Token (#{HttpHeaders::AUTH_TOKEN})"
      end

      def authenticate_with_system_token(x_miq_token)
        @miq_token_hash = YAML.load(ManageIQ::Password.decrypt(x_miq_token))

        validate_system_token_server(@miq_token_hash[:server_guid])
        validate_system_token_timestamp(@miq_token_hash[:timestamp])

        if @miq_token_hash[:user_metadata].present?
          User.authorize_user_with_system_token(@miq_token_hash[:userid], @miq_token_hash[:user_metadata])
        else
          User.authorize_user(@miq_token_hash[:userid])
        end

        auth_user(@miq_token_hash[:userid])
      rescue => err
        api_log_error("Authentication Failed with System Token\nX-MIQ-Token: #{x_miq_token}\nError: #{err}")
        raise AuthenticationError, "Invalid System Authentication Token specified"
      end

      def validate_system_token_server(server_guid)
        raise "Missing server_guid" if server_guid.blank?
        raise "Invalid server_guid #{server_guid} specified" unless MiqServer.where(:guid => server_guid).exists?
      end

      def validate_system_token_timestamp(timestamp)
        raise "Missing timestamp" if timestamp.blank?
        raise "Invalid timestamp #{timestamp} specified" if SYSTEM_TOKEN_TTL.ago.utc > timestamp
      end

      def valid_ui_session?
        [
          valid_authenticity_token?(session, request.x_csrf_token) || request.method == 'GET', # CSRF token be set and valid (or GET)
          session[:userid].present?, # session has a userid stored
          request.origin.nil? || request.origin == request.base_url # origin header if set matches base_url
        ].all?
      end

      def authenticate_with_jwt
        timeout = ::Settings.api.authentication_timeout.to_i_with_method
        user_header = request.headers["X-REMOTE-USER"]&.force_encoding("UTF-8")
        user = User.authenticate(user_header, "", request, :require_user => true, :timeout => timeout)
        auth_user(user.userid)
      rescue => e
        raise AuthenticationError, "Failed to Authenticate with JWT - error #{e}"
      end

      def basic_authentication(username, password)
        timeout = ::Settings.api.authentication_timeout.to_i_with_method
        user = User.authenticate(username, password, request, :require_user => true, :timeout => timeout)
        auth_user(user.userid)
      rescue MiqException::MiqEVMLoginError => e
        raise AuthenticationError, e.message
      end
    end
  end
end