# frozen_string_literal: true

module Gitlab
  module Auth
    # Keeps track of the current session user mode
    #
    # In order to perform administrative tasks over some interfaces,
    # an administrator must have explicitly enabled admin-mode
    # e.g. on web access require re-authentication
    class CurrentUserMode
      NotRequestedError = Class.new(StandardError)

      # RequestStore entries
      CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY = { res: :current_user_mode, data: :bypass_session_admin_id }.freeze
      CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY =         { res: :current_user_mode, data: :current_admin }.freeze

      # SessionStore entries
      SESSION_STORE_KEY = :current_user_mode
      ADMIN_MODE_START_TIME_KEY = :admin_mode
      ADMIN_MODE_REQUESTED_TIME_KEY = :admin_mode_requested
      MAX_ADMIN_MODE_TIME = 6.hours
      ADMIN_MODE_REQUESTED_GRACE_PERIOD = 5.minutes

      class << self
        # Admin mode activation requires storing a flag in the user session. Using this
        # method when scheduling jobs in sessionless environments (e.g. Sidekiq, API)
        # will bypass the session check for a user that was already in admin mode
        #
        # If passed a block, it will surround the block execution and reset the session
        # bypass at the end; otherwise you must remember to call '.reset_bypass_session!'
        def bypass_session!(admin_id)
          Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY] = admin_id
          # Bypassing the session invalidates the cached value of admin_mode?
          # Any new calls need to be re-computed.
          uncache_admin_mode_state(admin_id)

          Gitlab::AppLogger.debug("Bypassing session in admin mode for: #{admin_id}")

          return unless block_given?

          begin
            yield
          ensure
            reset_bypass_session!(admin_id)
          end
        end

        def reset_bypass_session!(admin_id = nil)
          # Restoring the session bypass invalidates the cached value of admin_mode?
          uncache_admin_mode_state(admin_id)
          Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY)
        end

        def bypass_session_admin_id
          Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY]
        end

        def uncache_admin_mode_state(admin_id = nil)
          if admin_id
            key = { res: :current_user_mode, user: admin_id, method: :admin_mode? }
            Gitlab::SafeRequestStore.delete(key)
          else
            Gitlab::SafeRequestStore.delete_if do |key|
              key.is_a?(Hash) && key[:res] == :current_user_mode && key[:method] == :admin_mode?
            end
          end
        end

        # Store in the current request the provided user model (only if in admin mode)
        # and yield
        def with_current_admin(admin)
          return yield unless new(admin).admin_mode?

          Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY] = admin

          Gitlab::AppLogger.debug("Admin mode active for: #{admin.username}")

          yield
        ensure
          Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY)
        end

        def current_admin
          Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY]
        end
      end

      def initialize(user)
        @user = user
      end

      def admin_mode?
        return false unless user

        Gitlab::SafeRequestStore.fetch(admin_mode_rs_key) do
          user.admin? && (privileged_runtime? || session_with_admin_mode?)
        end
      end

      def admin_mode_requested?
        return false unless user

        Gitlab::SafeRequestStore.fetch(admin_mode_requested_rs_key) do
          user.admin? && admin_mode_requested_in_grace_period?
        end
      end

      def enable_admin_mode!(password: nil, skip_password_validation: false)
        return unless user&.admin?
        return unless skip_password_validation || user&.valid_password?(password)

        raise NotRequestedError unless admin_mode_requested?

        reset_request_store_cache_entries

        current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
        current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now
      end

      def disable_admin_mode!
        return unless user&.admin?

        reset_request_store_cache_entries

        current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
        current_session_data[ADMIN_MODE_START_TIME_KEY] = nil
      end

      def request_admin_mode!
        return unless user&.admin?

        reset_request_store_cache_entries

        current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = Time.now
      end

      private

      attr_reader :user

      # RequestStore entry to cache #admin_mode? result
      def admin_mode_rs_key
        @admin_mode_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode? }
      end

      # RequestStore entry to cache #admin_mode_requested? result
      def admin_mode_requested_rs_key
        @admin_mode_requested_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode_requested? }
      end

      def current_session_data
        @current_session ||= Gitlab::NamespacedSessionStore.new(SESSION_STORE_KEY)
      end

      def session_with_admin_mode?
        return true if bypass_session?

        current_session_data.initiated? && current_session_data[ADMIN_MODE_START_TIME_KEY].to_i > MAX_ADMIN_MODE_TIME.ago.to_i
      end

      def admin_mode_requested_in_grace_period?
        current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY].to_i > ADMIN_MODE_REQUESTED_GRACE_PERIOD.ago.to_i
      end

      def bypass_session?
        user&.id && user.id == self.class.bypass_session_admin_id
      end

      def reset_request_store_cache_entries
        Gitlab::SafeRequestStore.delete(admin_mode_rs_key)
        Gitlab::SafeRequestStore.delete(admin_mode_requested_rs_key)
      end

      # Runtimes which imply shell access get admin mode automatically, see Gitlab::Runtime
      def privileged_runtime?
        Gitlab::Runtime.rake? || Gitlab::Runtime.rails_runner? || Gitlab::Runtime.console?
      end
    end
  end
end