# frozen_string_literal: true

# Copyright (C) 2014-2020 MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'mongo/auth/credential_cache'
require 'mongo/auth/stringprep'
require 'mongo/auth/conversation_base'
require 'mongo/auth/sasl_conversation_base'
require 'mongo/auth/scram_conversation_base'
require 'mongo/auth/user'
require 'mongo/auth/roles'
require 'mongo/auth/base'
require 'mongo/auth/aws'
require 'mongo/auth/cr'
require 'mongo/auth/gssapi'
require 'mongo/auth/ldap'
require 'mongo/auth/scram'
require 'mongo/auth/scram256'
require 'mongo/auth/x509'
require 'mongo/error/read_write_retryable'
require 'mongo/error/labelable'

module Mongo
  # This namespace contains all authentication related behavior.
  #
  # @since 2.0.0
  module Auth
    extend self

    # The external database name.
    #
    # @since 2.0.0
    # @api private
    EXTERNAL = '$external'

    # Constant for the nonce command.
    #
    # @since 2.0.0
    # @api private
    GET_NONCE = { getnonce: 1 }.freeze

    # Constant for the nonce field.
    #
    # @since 2.0.0
    # @api private
    NONCE = 'nonce'

    # Map the symbols parsed from the URI connection string to strategies.
    #
    # @note This map is not frozen because when mongo_kerberos is loaded,
    #   it mutates this map by adding the Kerberos authenticator.
    #
    # @since 2.0.0
    SOURCES = {
      aws: Aws,
      gssapi: Gssapi,
      mongodb_cr: CR,
      mongodb_x509: X509,
      plain: LDAP,
      scram: Scram,
      scram256: Scram256,
    }

    # Get an authenticator for the provided user to authenticate over the
    # provided connection.
    #
    # @param [ Auth::User ] user The user to authenticate.
    # @param [ Mongo::Connection ] connection The connection to authenticate over.
    #
    # @option opts [ String | nil ] speculative_auth_client_nonce The client
    #   nonce used in speculative auth on the specified connection that
    #   produced the specified speculative auth result.
    # @option opts [ BSON::Document | nil ] speculative_auth_result The
    #   value of speculativeAuthenticate field of hello response of
    #   the handshake on the specified connection.
    #
    # @return [ Auth::Aws | Auth::CR | Auth::Gssapi | Auth::LDAP |
    #   Auth::Scram | Auth::Scram256 | Auth::X509 ] The authenticator.
    #
    # @since 2.0.0
    # @api private
    def get(user, connection, **opts)
      mechanism = user.mechanism
      raise InvalidMechanism.new(mechanism) unless SOURCES.has_key?(mechanism)

      SOURCES[mechanism].new(user, connection, **opts)
    end

    # Raised when trying to authorize with an invalid configuration
    #
    # @since 2.11.0
    class InvalidConfiguration < Mongo::Error::AuthError; end

    # Raised when trying to get an invalid authorization mechanism.
    #
    # @since 2.0.0
    class InvalidMechanism < InvalidConfiguration
      # Instantiate the new error.
      #
      # @example Instantiate the error.
      #   Mongo::Auth::InvalidMechanism.new(:test)
      #
      # @param [ Symbol ] mechanism The provided mechanism.
      #
      # @since 2.0.0
      def initialize(mechanism)
        known_mechanisms = SOURCES.keys.sort.map do |key|
          key.inspect
        end.join(', ')
        super("#{mechanism.inspect} is invalid, please use one of the following mechanisms: #{known_mechanisms}")
      end
    end

    # Raised when a user is not authorized on a database.
    #
    # @since 2.0.0
    class Unauthorized < Mongo::Error::AuthError
      include Error::ReadWriteRetryable
      include Error::Labelable

      # @return [ Integer ] The error code.
      attr_reader :code

      # Instantiate the new error.
      #
      # @example Instantiate the error.
      #   Mongo::Auth::Unauthorized.new(user)
      #
      # @param [ Mongo::Auth::User ] user The unauthorized user.
      # @param [ String ] used_mechanism Auth mechanism actually used for
      #   authentication. This is a full string like SCRAM-SHA-256.
      # @param [ String ] message The error message returned by the server.
      # @param [ Server ] server The server instance that authentication
      #   was attempted against.
      # @param [ Integer ] The error code.
      #
      # @since 2.0.0
      def initialize(user, used_mechanism: nil, message: nil,
                     server: nil, code: nil)
        @code = code

        configured_bits = []
        used_bits = [
          "auth source: #{user.auth_source}",
        ]

        configured_bits << "mechanism: #{user.mechanism}" if user.mechanism

        used_bits << "used mechanism: #{used_mechanism}" if used_mechanism

        used_bits << "used server: #{server.address} (#{server.status})" if server

        used_user = if user.mechanism == :mongodb_x509
                      'Client certificate'
                    else
                      "User #{user.name}"
                    end

        configured_bits = if configured_bits.empty?
                            ''
                          else
                            " (#{configured_bits.join(', ')})"
                          end

        used_bits = " (#{used_bits.join(', ')})"

        msg = "#{used_user}#{configured_bits} is not authorized to access #{user.database}#{used_bits}"
        msg += ': ' + message if message
        super(msg)
      end
    end
  end
end
