summaryrefslogtreecommitdiff
path: root/lib/gitlab/auth/ldap/config.rb
blob: 47d63eb53cfff37df62179115360c7e17fc3fba5 (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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# frozen_string_literal: true

# Load a specific server configuration
module Gitlab
  module Auth
    module LDAP
      class Config
        NET_LDAP_ENCRYPTION_METHOD = {
          simple_tls: :simple_tls,
          start_tls:  :start_tls,
          plain:      nil
        }.freeze

        attr_accessor :provider, :options

        InvalidProvider = Class.new(StandardError)

        def self.enabled?
          Gitlab.config.ldap.enabled
        end

        def self.servers
          Gitlab.config.ldap['servers']&.values || []
        end

        def self.available_servers
          return [] unless enabled?

          _available_servers
        end

        def self._available_servers
          Array.wrap(servers.first)
        end

        def self.providers
          servers.map { |server| server['provider_name'] }
        end

        def self.valid_provider?(provider)
          providers.include?(provider)
        end

        def self.invalid_provider(provider)
          raise InvalidProvider.new("Unknown provider (#{provider}). Available providers: #{providers}")
        end

        def initialize(provider)
          if self.class.valid_provider?(provider)
            @provider = provider
          else
            self.class.invalid_provider(provider)
          end

          @options = config_for(@provider) # Use @provider, not provider
        end

        def enabled?
          base_config.enabled
        end

        def adapter_options
          opts = base_options.merge(
            encryption: encryption_options
          )

          opts.merge!(auth_options) if has_auth?

          opts
        end

        def omniauth_options
          opts = base_options.merge(
            base: base,
            encryption: options['encryption'],
            filter: omniauth_user_filter,
            name_proc: name_proc,
            disable_verify_certificates: !options['verify_certificates'],
            tls_options: tls_options
          )

          if has_auth?
            opts.merge!(
              bind_dn: options['bind_dn'],
              password: options['password']
            )
          end

          opts
        end

        def base
          @base ||= Person.normalize_dn(options['base'])
        end

        def uid
          options['uid']
        end

        def label
          options['label']
        end

        def sync_ssh_keys?
          sync_ssh_keys.present?
        end

        # The LDAP attribute in which the ssh keys are stored
        def sync_ssh_keys
          options['sync_ssh_keys']
        end

        def user_filter
          options['user_filter']
        end

        def constructed_user_filter
          @constructed_user_filter ||= Net::LDAP::Filter.construct(user_filter)
        end

        def group_base
          options['group_base']
        end

        def admin_group
          options['admin_group']
        end

        def active_directory
          options['active_directory']
        end

        def block_auto_created_users
          options['block_auto_created_users']
        end

        def attributes
          default_attributes.merge(options['attributes'])
        end

        def timeout
          options['timeout'].to_i
        end

        def external_groups
          options['external_groups'] || []
        end

        def has_auth?
          options['password'] || options['bind_dn']
        end

        def allow_username_or_email_login
          options['allow_username_or_email_login']
        end

        def lowercase_usernames
          options['lowercase_usernames']
        end

        def name_proc
          if allow_username_or_email_login
            proc { |name| name.gsub(/@.*\z/, '') }
          else
            proc { |name| name }
          end
        end

        def default_attributes
          {
            'username'    => %w(uid sAMAccountName userid),
            'email'       => %w(mail email userPrincipalName),
            'name'        => 'cn',
            'first_name'  => 'givenName',
            'last_name'   => 'sn'
          }
        end

        protected

        def base_options
          {
            host: options['host'],
            port: options['port']
          }
        end

        def base_config
          Gitlab.config.ldap
        end

        def config_for(provider)
          base_config.servers.values.find { |server| server['provider_name'] == provider }
        end

        def encryption_options
          method = translate_method
          return unless method

          {
            method: method,
            tls_options: tls_options
          }
        end

        def translate_method
          NET_LDAP_ENCRYPTION_METHOD[options['encryption']&.to_sym]
        end

        def tls_options
          return @tls_options if defined?(@tls_options)

          method = translate_method
          return unless method

          opts = if options['verify_certificates'] && method != 'plain'
                   # Dup so we don't accidentally overwrite the constant
                   OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.dup
                 else
                   # It is important to explicitly set verify_mode for two reasons:
                   # 1. The behavior of OpenSSL is undefined when verify_mode is not set.
                   # 2. The net-ldap gem implementation verifies the certificate hostname
                   #    unless verify_mode is set to VERIFY_NONE.
                   { verify_mode: OpenSSL::SSL::VERIFY_NONE }
                 end

          opts.merge!(custom_tls_options)

          @tls_options = opts
        end

        def custom_tls_options
          return {} unless options['tls_options']

          # Dup so we don't overwrite the original value
          custom_options = options['tls_options'].dup.delete_if { |_, value| value.nil? || value.blank? }
          custom_options.symbolize_keys!

          if custom_options[:cert]
            begin
              custom_options[:cert] = OpenSSL::X509::Certificate.new(custom_options[:cert])
            rescue OpenSSL::X509::CertificateError => e
              Rails.logger.error "LDAP TLS Options 'cert' is invalid for provider #{provider}: #{e.message}"
            end
          end

          if custom_options[:key]
            begin
              custom_options[:key] = OpenSSL::PKey.read(custom_options[:key])
            rescue OpenSSL::PKey::PKeyError => e
              Rails.logger.error "LDAP TLS Options 'key' is invalid for provider #{provider}: #{e.message}"
            end
          end

          custom_options
        end

        def auth_options
          {
            auth: {
              method: :simple,
              username: options['bind_dn'],
              password: options['password']
            }
          }
        end

        def omniauth_user_filter
          uid_filter = Net::LDAP::Filter.eq(uid, '%{username}')

          if user_filter.present?
            Net::LDAP::Filter.join(uid_filter, constructed_user_filter).to_s
          else
            uid_filter.to_s
          end
        end
      end
    end
  end
end