File: adaptor.rb

package info (click to toggle)
ruby-omniauth-ldap 2.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 180 kB
  • sloc: ruby: 720; makefile: 3
file content (235 lines) | stat: -rw-r--r-- 7,815 bytes parent folder | download | duplicates (2)
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
#this code borrowed pieces from activeldap and net-ldap

require 'rack'
require 'net/ldap'
require 'net/ntlm'
require 'sasl'
require 'kconv'
module OmniAuth
  module LDAP
    class Adaptor
      class LdapError < StandardError; end
      class ConfigurationError < StandardError; end
      class AuthenticationError < StandardError; end
      class ConnectionError < StandardError; end

      VALID_ADAPTER_CONFIGURATION_KEYS = [
        :hosts, :host, :port, :encryption, :disable_verify_certificates, :bind_dn, :password, :try_sasl,
        :sasl_mechanisms, :uid, :base, :allow_anonymous, :filter, :tls_options,

        # Deprecated
        :method,
        :ca_file,
        :ssl_version
      ]

      # A list of needed keys. Possible alternatives are specified using sub-lists.
      MUST_HAVE_KEYS = [
        :base,
        [:encryption, :method], # :method is deprecated
        [:hosts, :host],
        [:hosts, :port],
        [:uid, :filter]
      ]

      ENCRYPTION_METHOD = {
        :simple_tls => :simple_tls,
        :start_tls => :start_tls,
        :plain => nil,

        # Deprecated. This mapping aimed to be user-friendly, but only caused
        # confusion. Better to pass-through the actual `Net::LDAP` encryption type.
        :ssl => :simple_tls,
        :tls => :start_tls,
      }

      attr_accessor :bind_dn, :password
      attr_reader :connection, :uid, :base, :auth, :filter

      def self.validate(configuration={})
        message = []
        MUST_HAVE_KEYS.each do |names|
          names = [names].flatten
          missing_keys = names.select{|name| configuration[name].nil?}
          if missing_keys == names
            message << names.join(' or ')
          end
        end
        raise ArgumentError.new(message.join(",") +" MUST be provided") unless message.empty?
      end

      def initialize(configuration={})
        Adaptor.validate(configuration)
        @configuration = configuration.dup
        @configuration[:allow_anonymous] ||= false
        @logger = @configuration.delete(:logger)
        VALID_ADAPTER_CONFIGURATION_KEYS.each do |name|
          instance_variable_set("@#{name}", @configuration[name])
        end
        config = {
          base: @base,
          hosts: @hosts,
          host: @host,
          port: @port,
          encryption: encryption_options
        }
        @bind_method = @try_sasl ? :sasl : (@allow_anonymous||!@bind_dn||!@password ? :anonymous : :simple)


        @auth = sasl_auths({:username => @bind_dn, :password => @password}).first if @bind_method == :sasl
        @auth ||= { :method => @bind_method,
                    :username => @bind_dn,
                    :password => @password
                  }
        config[:auth] = @auth
        @connection = Net::LDAP.new(config)
      end

      #:base => "dc=yourcompany, dc=com",
      # :filter => "(mail=#{user})",
      # :password => psw
      def bind_as(args = {})
        result = false
        @connection.open do |me|
          rs = me.search args
          if rs and rs.first and dn = rs.first.dn
            password = args[:password]
            method = args[:method] || @method
            password = password.call if password.respond_to?(:call)
            if method == 'sasl'
            result = rs.first if me.bind(sasl_auths({:username => dn, :password => password}).first)
            else
            result = rs.first if me.bind(:method => :simple, :username => dn,
                                :password => password)
            end
          end
        end
        result
      end

      private

      def encryption_options
        translated_method = translate_method
        return nil unless translated_method

        {
          method: translated_method,
          tls_options: tls_options(translated_method)
        }
      end

      def translate_method
        method = @encryption || @method
        method ||= "plain"
        normalized_method = method.to_s.downcase.to_sym

        unless ENCRYPTION_METHOD.has_key?(normalized_method)
          available_methods = ENCRYPTION_METHOD.keys.collect {|m| m.inspect}.join(", ")
          format = "%s is not one of the available connect methods: %s"
          raise ConfigurationError, format % [method.inspect, available_methods]
        end

        ENCRYPTION_METHOD[normalized_method]
      end

      def tls_options(translated_method)
        return {} if translated_method == nil # (plain)

        options = default_options

        if @tls_options
          # Prevent blank config values from overwriting SSL defaults
          configured_options = sanitize_hash_values(@tls_options)
          configured_options = symbolize_hash_keys(configured_options)

          options.merge!(configured_options)
        end

        # Retain backward compatibility until deprecated configs are removed.
        options[:ca_file] = @ca_file if @ca_file
        options[:ssl_version] = @ssl_version if @ssl_version

        options
      end

      def sasl_auths(options={})
        auths = []
        sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms
        sasl_mechanisms.each do |mechanism|
          normalized_mechanism = mechanism.downcase.gsub(/-/, '_')
          sasl_bind_setup = "sasl_bind_setup_#{normalized_mechanism}"
          next unless respond_to?(sasl_bind_setup, true)
          initial_credential, challenge_response = send(sasl_bind_setup, options)
          auths << {
            :method => :sasl,
            :initial_credential => initial_credential,
            :mechanism => mechanism,
            :challenge_response => challenge_response
          }
        end
        auths
      end

      def sasl_bind_setup_digest_md5(options)
        bind_dn = options[:username]
        initial_credential = ""
        challenge_response = Proc.new do |cred|
          pref = SASL::Preferences.new :digest_uri => "ldap/#{@host}", :username => bind_dn, :has_password? => true, :password => options[:password]
          sasl = SASL.new("DIGEST-MD5", pref)
          response = sasl.receive("challenge", cred)
          response[1]
        end
        [initial_credential, challenge_response]
      end

      def sasl_bind_setup_gss_spnego(options)
        bind_dn = options[:username]
        psw = options[:password]
        raise LdapError.new( "invalid binding information" ) unless (bind_dn && psw)

        nego = proc {|challenge|
          t2_msg = Net::NTLM::Message.parse( challenge )
          bind_dn, domain = bind_dn.split('\\').reverse
          t2_msg.target_name = Net::NTLM::encode_utf16le(domain) if domain
          t3_msg = t2_msg.response( {:user => bind_dn, :password => psw}, {:ntlmv2 => true} )
          t3_msg.serialize
        }
        [Net::NTLM::Message::Type1.new.serialize, nego]
      end

      private

      def default_options
        if @disable_verify_certificates
          # 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 }
        else
          OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.dup
        end
      end

      # Removes keys that have blank values
      #
      # This gem may not always be in the context of Rails so we
      # do this rather than `.blank?`.
      def sanitize_hash_values(hash)
        hash.delete_if do |_, value|
          value.nil? ||
          (value.is_a?(String) && value !~ /\S/)
        end
      end

      def symbolize_hash_keys(hash)
        hash.keys.each do |key|
          hash[key.to_sym] = hash[key]
        end

        hash
      end
    end
  end
end