Class: OmniAuth::LDAP::Adaptor

Inherits:
Object
  • Object
show all
Defined in:
lib/omniauth-ldap/adaptor.rb

Overview

Note:

Public API: Adaptor.validate, #initialize, #bind_as, and attr readers such as #connection, #uid

Adaptor encapsulates the behavior required to connect to an LDAP server and perform searches and binds. It maps user-provided configuration into a Net::LDAP connection and provides compatibility helpers for different net-ldap and SASL versions. The adaptor is intentionally defensive and provides a small, stable API used by the OmniAuth strategy.

Examples:

Initialize with minimal config

adaptor = OmniAuth::LDAP::Adaptor.new(base: 'dc=example,dc=com', host: 'ldap.example.com')

Defined Under Namespace

Classes: AuthenticationError, ConfigurationError, ConnectionError, LdapError

Constant Summary collapse

VALID_ADAPTER_CONFIGURATION_KEYS =

Valid configuration keys accepted by the adaptor. These correspond to the options supported by the gem and are used during initialization.

Returns:

  • (Array<Symbol>)
[
  :hosts,
  :host,
  :port,
  :encryption,
  :disable_verify_certificates,
  :bind_dn,
  :password,
  :try_sasl,
  :sasl_mechanisms,
  :uid,
  :base,
  :allow_anonymous,
  :filter,
  :tls_options,
  :password_policy,
  # Timeouts
  :connect_timeout,
  :read_timeout,

  # Deprecated
  :method,
  :ca_file,
  :ssl_version,
]
MUST_HAVE_KEYS =

Required configuration keys. This may include alternatives as sub-lists (e.g., [:hosts, :host] means either key is acceptable).

Returns:

  • (Array)
[
  :base,
  [:encryption, :method], # :method is deprecated
  [:hosts, :host],
  [:hosts, :port],
  [:uid, :filter],
]
ENCRYPTION_METHOD =

Supported encryption method mapping for configuration readability.

Returns:

  • (Hash<Symbol,Symbol,nil>)
{
  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,
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(configuration = {}) ⇒ OmniAuth::LDAP::Adaptor

Create a new adaptor instance backed by a Net::LDAP connection.

The constructor does not immediately open a network connection but prepares the Net::LDAP instance according to the provided configuration. It also applies timeout settings where supported by the installed net-ldap version.

Parameters:

  • configuration (Hash) (defaults to: {})

    user-provided configuration options

Raises:



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
# File 'lib/omniauth-ldap/adaptor.rb', line 155

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,
  }
  # Remove passing timeouts here to avoid issues on older net-ldap versions.
  # We'll set them after initialization if the connection responds to writers.
  @bind_method = if @try_sasl
    :sasl
  else
    ((@allow_anonymous || !@bind_dn || !@password) ? :anonymous : :simple)
  end

  @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)
  # Apply optional timeout settings if supported by the installed net-ldap version
  if !@connect_timeout.nil?
    if @connection.respond_to?(:connect_timeout=)
      @connection.connect_timeout = @connect_timeout
    else
      @connection.instance_variable_set(:@connect_timeout, @connect_timeout)
    end
  end
  if !@read_timeout.nil?
    if @connection.respond_to?(:read_timeout=)
      @connection.read_timeout = @read_timeout
    else
      @connection.instance_variable_set(:@read_timeout, @read_timeout)
    end
  end
end

Instance Attribute Details

#authHash (readonly)

The final auth structure used by net-ldap.

Returns:

  • (Hash)


126
# File 'lib/omniauth-ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

#baseString (readonly)

The base DN for searches.

Returns:

  • (String)


126
# File 'lib/omniauth-ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

#bind_dnString?

The distinguished name used for binding when provided in configuration.

Returns:

  • (String, nil)


99
100
101
# File 'lib/omniauth-ldap/adaptor.rb', line 99

def bind_dn
  @bind_dn
end

#connectionNet::LDAP (readonly)

The underlying Net::LDAP connection object.

Returns:

  • (Net::LDAP)


126
127
128
# File 'lib/omniauth-ldap/adaptor.rb', line 126

def connection
  @connection
end

#filterString? (readonly)

Custom filter pattern when provided in configuration.

Returns:

  • (String, nil)


126
# File 'lib/omniauth-ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

#last_operation_resultObject? (readonly)

Last operation result object returned by the ldap library (if any)

Returns:

  • (Object, nil)


126
# File 'lib/omniauth-ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

#last_password_policy_responseObject (readonly)

Read-only attributes exposing connection and configuration state.



126
# File 'lib/omniauth-ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

#passwordString?

The bind password (may be nil for anonymous binds)

Returns:

  • (String, nil)


99
# File 'lib/omniauth-ldap/adaptor.rb', line 99

attr_accessor :bind_dn, :password

#password_policyBoolean (readonly)

Whether to request LDAP Password Policy controls.

Returns:

  • (Boolean)


126
# File 'lib/omniauth-ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

#uidString (readonly)

The user id attribute used for lookups (e.g., ‘sAMAccountName’)

Returns:

  • (String)


126
# File 'lib/omniauth-ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

Class Method Details

.validate(configuration = {}) ⇒ void

This method returns an undefined value.

Validate that a minimal configuration is present. Raises ArgumentError when required keys are missing. This is a convenience to provide early feedback to callers.

Parameters:

  • configuration (Hash) (defaults to: {})

    configuration hash passed to the adaptor

Raises:

  • (ArgumentError)

    when required keys are missing



134
135
136
137
138
139
140
141
142
143
144
# File 'lib/omniauth-ldap/adaptor.rb', line 134

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

Instance Method Details

#bind_as(args = {}) ⇒ Net::LDAP::Entry, ...

:base => “dc=yourcompany, dc=com”, :filter => “(mail=#user)”, :password => psw

Attempt to locate a user entry and bind as that entry using the supplied password. Returns the entry on success, or false/nil on failure.

Parameters:

  • args (Hash) (defaults to: {})

    search and bind options forwarded to net-ldap’s search

Options Hash (args):

  • :filter (Net::LDAP::Filter, String)

    LDAP filter to use

  • :size (Integer)

    maximum number of results to fetch

  • :password (String, Proc)

    a password string or callable returning a password

Returns:

  • (Net::LDAP::Entry, false, nil)

    the found entry on successful bind, otherwise false/nil

Raises:



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
# File 'lib/omniauth-ldap/adaptor.rb', line 216

def bind_as(args = {})
  result = false
  @last_operation_result = nil
  @last_password_policy_response = nil
  @connection.open do |me|
    rs = me.search(args)
    raise ConnectionError.new("LDAP search operation failed") unless rs

    if rs && rs.first
      dn = rs.first.dn
      if dn
        password = args[:password]
        password = password.call if password.respond_to?(:call)

        bind_args = if @bind_method == :sasl
          sasl_auths({username: dn, password: password}).first
        else
          {
            method: :simple,
            username: dn,
            password: password,
          }
        end

        # Optionally request LDAP Password Policy control (RFC Draft - de facto standard)
        if @password_policy
          # Always request by OID using a simple hash; avoids depending on gem-specific control classes
          control = {oid: "1.3.6.1.4.1.42.2.27.8.5.1", criticality: true, value: nil}
          if bind_args.is_a?(Hash)
            bind_args = bind_args.merge({controls: [control]})
          else
            # Some Net::LDAP versions allow passing a block for SASL only; ensure we still can add controls if hash
            # When not a Hash, we can't merge; rely on server default behavior.
          end
        end

        begin
          success = bind_args ? me.bind(bind_args) : me.bind
        ensure
          capture_password_policy(me)
        end

        result = rs.first if success
      end
    end
  end
  result
end