summaryrefslogtreecommitdiff
path: root/lib/gitlab/auth/ldap/adapter.rb
blob: 15b9d5ad6e92f6b4625c91ad9ec904495a847492 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# frozen_string_literal: true

module Gitlab
  module Auth
    module LDAP
      class Adapter
        SEARCH_RETRY_FACTOR = [1, 1, 2, 3].freeze
        MAX_SEARCH_RETRIES = Rails.env.test? ? 1 : SEARCH_RETRY_FACTOR.size.freeze

        attr_reader :provider, :ldap

        def self.open(provider, &block)
          Net::LDAP.open(config(provider).adapter_options) do |ldap|
            block.call(self.new(provider, ldap))
          end
        end

        def self.config(provider)
          Gitlab::Auth::LDAP::Config.new(provider)
        end

        def initialize(provider, ldap = nil)
          @provider = provider
          @ldap = ldap || renew_connection_adapter
        end

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

        def users(fields, value, limit = nil)
          options = user_options(Array(fields), value, limit)
          users_search(options)
        end

        def user(*args)
          users(*args).first
        end

        def dn_matches_filter?(dn, filter)
          ldap_search(base: dn,
                      filter: filter,
                      scope: Net::LDAP::SearchScope_BaseObject,
                      attributes: %w{dn}).any?
        end

        def ldap_search(*args)
          retries ||= 0

          # Net::LDAP's `time` argument doesn't work. Use Ruby `Timeout` instead.
          Timeout.timeout(timeout_time(retries)) do
            results = ldap.search(*args)

            if results.nil?
              response = ldap.get_operation_result

              unless response.code.zero?
                Rails.logger.warn("LDAP search error: #{response.message}")
              end

              []
            else
              results
            end
          end
        rescue Net::LDAP::Error, Timeout::Error => error
          retries += 1
          error_message = connection_error_message(error)

          Rails.logger.warn(error_message)

          if retries < MAX_SEARCH_RETRIES
            renew_connection_adapter
            retry
          else
            raise LDAPConnectionError, error_message
          end
        end

        private

        def timeout_time(retry_number)
          SEARCH_RETRY_FACTOR[retry_number] * config.timeout
        end

        def users_search(options)
          entries = ldap_search(options).select do |entry|
            entry.respond_to? config.uid
          end

          entries.map do |entry|
            Gitlab::Auth::LDAP::Person.new(entry, provider)
          end
        end

        def user_options(fields, value, limit)
          options = {
            attributes: Gitlab::Auth::LDAP::Person.ldap_attributes(config),
            base: config.base
          }

          options[:size] = limit if limit

          if fields.include?('dn')
            raise ArgumentError, 'It is not currently possible to search the DN and other fields at the same time.' if fields.size > 1

            options[:base] = value
            options[:scope] = Net::LDAP::SearchScope_BaseObject
          else
            filter = fields.map { |field| Net::LDAP::Filter.eq(field, value) }.inject(:|)
          end

          options.merge(filter: user_filter(filter))
        end

        def user_filter(filter = nil)
          user_filter = config.constructed_user_filter if config.user_filter.present?

          if user_filter && filter
            Net::LDAP::Filter.join(filter, user_filter)
          elsif user_filter
            user_filter
          else
            filter
          end
        end

        def connection_error_message(exception)
          if exception.is_a?(Timeout::Error)
            "LDAP search timed out after #{config.timeout} seconds"
          else
            "LDAP search raised exception #{exception.class}: #{exception.message}"
          end
        end

        def renew_connection_adapter
          @ldap = Net::LDAP.new(config.adapter_options)
        end
      end
    end
  end
end