module Gitlab
  module Auth
    module LDAP
      class Person
        # Active Directory-specific LDAP filter that checks if bit 2 of the
        # userAccountControl attribute is set.
        # Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/
        AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2")

        InvalidEntryError = Class.new(StandardError)

        attr_accessor :entry, :provider

        def self.find_by_uid(uid, adapter)
          uid = Net::LDAP::Filter.escape(uid)
          adapter.user(adapter.config.uid, uid)
        end

        def self.find_by_dn(dn, adapter)
          adapter.user('dn', dn)
        end

        def self.find_by_email(email, adapter)
          email_fields = adapter.config.attributes['email']

          adapter.user(email_fields, email)
        end

        def self.disabled_via_active_directory?(dn, adapter)
          adapter.dn_matches_filter?(dn, AD_USER_DISABLED)
        end

        def self.ldap_attributes(config)
          [
            'dn',
            config.uid,
            *config.attributes['name'],
            *config.attributes['email'],
            *config.attributes['username']
          ].compact.uniq
        end

        def self.normalize_dn(dn)
          ::Gitlab::Auth::LDAP::DN.new(dn).to_normalized_s
        rescue ::Gitlab::Auth::LDAP::DN::FormatError => e
          Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}")

          dn
        end

        # Returns the UID in a normalized form.
        #
        # 1. Excess spaces are stripped
        # 2. The string is downcased (for case-insensitivity)
        def self.normalize_uid(uid)
          ::Gitlab::Auth::LDAP::DN.normalize_value(uid)
        rescue ::Gitlab::Auth::LDAP::DN::FormatError => e
          Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}")

          uid
        end

        def initialize(entry, provider)
          Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
          @entry = entry
          @provider = provider
        end

        def name
          attribute_value(:name).first
        end

        def uid
          entry.public_send(config.uid).first # rubocop:disable GitlabSecurity/PublicSend
        end

        def username
          username = attribute_value(:username)

          # Depending on the attribute, multiple values may
          # be returned. We need only one for username.
          # Ex. `uid` returns only one value but `mail` may
          # return an array of multiple email addresses.
          [username].flatten.first.tap do |username|
            username.downcase! if config.lowercase_usernames
          end
        end

        def email
          attribute_value(:email)
        end

        def dn
          self.class.normalize_dn(entry.dn)
        end

        private

        def entry
          @entry
        end

        def config
          @config ||= Gitlab::Auth::LDAP::Config.new(provider)
        end

        # Using the LDAP attributes configuration, find and return the first
        # attribute with a value. For example, by default, when given 'email',
        # this method looks for 'mail', 'email' and 'userPrincipalName' and
        # returns the first with a value.
        def attribute_value(attribute)
          attributes = Array(config.attributes[attribute.to_s])
          selected_attr = attributes.find { |attr| entry.respond_to?(attr) }

          return nil unless selected_attr

          entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend
        end
      end
    end
  end
end
