Class: GitHub::Ldap

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Instrumentation
Defined in:
lib/github/ldap.rb,
lib/github/ldap/url.rb,
lib/github/ldap/group.rb,
lib/github/ldap/domain.rb,
lib/github/ldap/filter.rb,
lib/github/ldap/server.rb,
lib/github/ldap/posix_group.rb,
lib/github/ldap/virtual_group.rb,
lib/github/ldap/instrumentation.rb,
lib/github/ldap/referral_chaser.rb,
lib/github/ldap/connection_cache.rb,
lib/github/ldap/member_search/base.rb,
lib/github/ldap/virtual_attributes.rb,
lib/github/ldap/user_search/default.rb,
lib/github/ldap/member_search/classic.rb,
lib/github/ldap/member_search/recursive.rb,
lib/github/ldap/membership_validators/base.rb,
lib/github/ldap/user_search/active_directory.rb,
lib/github/ldap/membership_validators/classic.rb,
lib/github/ldap/member_search/active_directory.rb,
lib/github/ldap/membership_validators/recursive.rb,
lib/github/ldap/membership_validators/active_directory.rb

Defined Under Namespace

Modules: Filter, Instrumentation, MemberSearch, MembershipValidators, UserSearch Classes: ConnectionCache, Domain, Group, PosixGroup, ReferralChaser, URL, VirtualAttributes, VirtualGroup

Constant Summary collapse

ACTIVE_DIRECTORY_V51_OID =

Internal: The capability required to use ActiveDirectory features. See: msdn.microsoft.com/en-us/library/cc223359.aspx.

"1.2.840.113556.1.4.1670".freeze
DEFAULT_FIXTURES_PATH =

Preconfigured user fixtures. If you want to use them for your own tests.

File.expand_path('fixtures.ldif', File.dirname(__FILE__))
DEFAULT_SERVER_OPTIONS =
{
  user_fixtures:  DEFAULT_FIXTURES_PATH,
  user_domain:    'dc=github,dc=com',
  admin_user:     'uid=admin,dc=github,dc=com',
  admin_password: 'secret',
  quiet:          true,
  port:           3897
}

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Ldap

Build a new GitHub::Ldap instance

## Connection

host: required string ldap server host address port: required string or number ldap server port hosts: an enumerable of pairs of hosts and corresponding ports with

which to attempt opening connections (default [[host, port]]). Overrides
host and port if set.

encryption: optional string. ‘ssl` or `tls`. nil by default admin_user: optional string ldap administrator user dn for authentication admin_password: optional string ldap administrator user password

## Behavior

uid: optional field name used to authenticate users. Defaults to ‘sAMAccountName` (what ActiveDirectory uses) virtual_attributes: optional. boolean true to use server’s virtual attributes. Hash to specify custom mapping. Default false. recursive_group_search_fallback: optional boolean whether membership checks should recurse into nested groups when virtual attributes aren’t enabled. Default false. posix_support: optional boolean ‘posixGroup` support. Default true. search_domains: optional array of string bases to search through

## Diagnostics

instrumentation_service: optional ActiveSupport::Notifications compatible object



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
# File 'lib/github/ldap.rb', line 83

def initialize(options = {})
  @uid = options[:uid] || "sAMAccountName"

  # Keep a reference to these as default auth for a Global Catalog if needed
  @admin_user = options[:admin_user]
  @admin_password = options[:admin_password]
  @port = options[:port]

  @connection = Net::LDAP.new({
    host: options[:host],
    port: options[:port],
    hosts: options[:hosts],
    instrumentation_service: options[:instrumentation_service]
  })

  if options[:admin_user] && options[:admin_password]
    @connection.authenticate(options[:admin_user], options[:admin_password])
  end

  if encryption = check_encryption(options[:encryption])
    @connection.encryption(encryption)
  end

  configure_virtual_attributes(options[:virtual_attributes])

  # enable fallback recursive group search unless option is false
  @recursive_group_search_fallback = (options[:recursive_group_search_fallback] != false)

  # enable posixGroup support unless option is false
  @posix_support = (options[:posix_support] != false)

  # search_domains is a connection of bases to perform searches
  # when a base is not explicitly provided.
  @search_domains = Array(options[:search_domains])

  # configure both the membership validator and the member search strategies
  configure_search_strategy(options[:search_strategy])

  # configure the strategy used by Domain#user? to look up a user entry for login
  configure_user_search_strategy(options[:user_search_strategy])

  # enables instrumenting queries
  @instrumentation_service = options[:instrumentation_service]
end

Class Attribute Details

.ldap_serverObject (readonly)

ldap_server: is the instance of the testing ldap server,

you should never interact with it,
but it's used to grecefully stop it after your tests finalize.


26
27
28
# File 'lib/github/ldap/server.rb', line 26

def ldap_server
  @ldap_server
end

.server_optionsObject (readonly)

server_options: is the options used to start the server,

useful to know in development.


21
22
23
# File 'lib/github/ldap/server.rb', line 21

def server_options
  @server_options
end

Instance Attribute Details

#admin_passwordObject (readonly)

Returns the value of attribute admin_password.



48
49
50
# File 'lib/github/ldap.rb', line 48

def admin_password
  @admin_password
end

#admin_userObject (readonly)

Returns the value of attribute admin_user.



48
49
50
# File 'lib/github/ldap.rb', line 48

def admin_user
  @admin_user
end

#connectionObject (readonly)

Returns the value of attribute connection.



48
49
50
# File 'lib/github/ldap.rb', line 48

def connection
  @connection
end

#instrumentation_serviceObject (readonly)

Returns the value of attribute instrumentation_service.



48
49
50
# File 'lib/github/ldap.rb', line 48

def instrumentation_service
  @instrumentation_service
end

#member_search_strategyObject (readonly)

Returns the value of attribute member_search_strategy.



48
49
50
# File 'lib/github/ldap.rb', line 48

def member_search_strategy
  @member_search_strategy
end

#membership_validatorObject (readonly)

Returns the value of attribute membership_validator.



48
49
50
# File 'lib/github/ldap.rb', line 48

def membership_validator
  @membership_validator
end

#portObject (readonly)

Returns the value of attribute port.



48
49
50
# File 'lib/github/ldap.rb', line 48

def port
  @port
end

#search_domainsObject (readonly)

Returns the value of attribute search_domains.



48
49
50
# File 'lib/github/ldap.rb', line 48

def search_domains
  @search_domains
end

#uidObject (readonly)

Returns the value of attribute uid.



48
49
50
# File 'lib/github/ldap.rb', line 48

def uid
  @uid
end

#user_search_strategyObject (readonly)

Returns the value of attribute user_search_strategy.



48
49
50
# File 'lib/github/ldap.rb', line 48

def user_search_strategy
  @user_search_strategy
end

#virtual_attributesObject (readonly)

Returns the value of attribute virtual_attributes.



48
49
50
# File 'lib/github/ldap.rb', line 48

def virtual_attributes
  @virtual_attributes
end

Class Method Details

.server_tmpObject

Determine the temporal directory where the ldap server lives. If there is no temporal directory in the environment we create one in the base path.

Returns the path to the temporal directory.



57
58
59
60
61
62
63
64
65
66
# File 'lib/github/ldap/server.rb', line 57

def self.server_tmp
  tmp = ENV['TMPDIR'] || ENV['TEMPDIR']

  if tmp.nil?
    tmp = 'tmp'
    Dir.mkdir(tmp) unless File.directory?('tmp')
  end

  tmp
end

.start_server(options = {}) ⇒ Object

Start a testing server. If there is already a server initialized it doesn’t do anything.

options: is a hash with the custom options for the server.



33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/github/ldap/server.rb', line 33

def self.start_server(options = {})
  @server_options = DEFAULT_SERVER_OPTIONS.merge(options)

  @server_options[:allow_anonymous] ||= false
  @server_options[:ldif]              = @server_options[:user_fixtures]
  @server_options[:domain]            = @server_options[:user_domain]
  @server_options[:tmpdir]          ||= server_tmp

  @server_options[:quiet] = false if @server_options[:verbose]

  @ldap_server = Ladle::Server.new(@server_options)
  @ldap_server.start
end

.stop_serverObject

Stop the testing server. If there is no server started this method doesn’t do anything.



49
50
51
# File 'lib/github/ldap/server.rb', line 49

def self.stop_server
  ldap_server && ldap_server.stop
end

Instance Method Details

#capabilitiesObject

Internal: Searches the host LDAP server’s Root DSE for capabilities and extensions.

Returns a Net::LDAP::Entry object.



223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/github/ldap.rb', line 223

def capabilities
  @capabilities ||=
    instrument "capabilities.github_ldap" do |payload|
      begin
        @connection.search_root_dse
      rescue Net::LDAP::LdapError => error
        payload[:error] = error
        # stubbed result
        Net::LDAP::Entry.new
      end
    end
end

#check_encryption(encryption) ⇒ Object

Internal - Determine whether to use encryption or not.

encryption: is the encryption method, either ‘ssl’, ‘tls’, ‘simple_tls’ or ‘start_tls’.

Returns the real encryption type.



241
242
243
244
245
246
247
248
249
250
# File 'lib/github/ldap.rb', line 241

def check_encryption(encryption)
  return unless encryption

  case encryption.downcase.to_sym
  when :ssl, :simple_tls
    :simple_tls
  when :tls, :start_tls
    :start_tls
  end
end

#configure_member_search_strategy(strategy = nil) ⇒ Object

Internal: Configure the member search strategy.

If no known strategy is provided, detects ActiveDirectory capabilities or falls back to the Recursive strategy by default.

Returns the selected strategy Class.



336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/github/ldap.rb', line 336

def configure_member_search_strategy(strategy = nil)
  @member_search_strategy =
    case strategy.to_s
    when "classic"
      GitHub::Ldap::MemberSearch::Classic
    when "recursive"
      GitHub::Ldap::MemberSearch::Recursive
    when "active_directory"
      GitHub::Ldap::MemberSearch::ActiveDirectory
    else
      # fallback to detection, defaulting to recursive strategy
      if active_directory_capability?
        GitHub::Ldap::MemberSearch::ActiveDirectory
      else
        GitHub::Ldap::MemberSearch::Recursive
      end
    end
end

#configure_membership_validation_strategy(strategy = nil) ⇒ Object

Internal: Configure the membership validation strategy.

If no known strategy is provided, detects ActiveDirectory capabilities or falls back to the Recursive strategy by default.

Returns the membership validator strategy Class.



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/github/ldap.rb', line 288

def configure_membership_validation_strategy(strategy = nil)
  @membership_validator =
    case strategy.to_s
    when "classic"
      GitHub::Ldap::MembershipValidators::Classic
    when "recursive"
      GitHub::Ldap::MembershipValidators::Recursive
    when "active_directory"
      GitHub::Ldap::MembershipValidators::ActiveDirectory
    else
      # fallback to detection, defaulting to recursive strategy
      if active_directory_capability?
        GitHub::Ldap::MembershipValidators::ActiveDirectory
      else
        GitHub::Ldap::MembershipValidators::Recursive
      end
    end
end

#configure_search_strategy(strategy = nil) ⇒ Object

Internal: Configure the member search and membership validation strategies.

TODO: Inline the logic in these two methods here.

Returns nothing.



274
275
276
277
278
279
280
# File 'lib/github/ldap.rb', line 274

def configure_search_strategy(strategy = nil)
  # configure which strategy should be used to validate user membership
  configure_membership_validation_strategy(strategy)

  # configure which strategy should be used for member search
  configure_member_search_strategy(strategy)
end

#configure_user_search_strategy(strategy) ⇒ Object

Internal: Set the user search strategy that will be used by

Domain#user?.

strategy - Can be either ‘default’ or ‘global_catalog’.

'default' strategy will search the configured
domain controller with a search base relative
to the controller's domain context.
'global_catalog' will search the entire forest
using Active Directory's Global Catalog
functionality.


317
318
319
320
321
322
323
324
325
326
327
# File 'lib/github/ldap.rb', line 317

def configure_user_search_strategy(strategy)
  @user_search_strategy =
    case strategy.to_s
    when "default"
      GitHub::Ldap::UserSearch::Default.new(self)
    when "global_catalog"
      GitHub::Ldap::UserSearch::ActiveDirectory.new(self)
    else
      GitHub::Ldap::UserSearch::Default.new(self)
    end
end

#configure_virtual_attributes(attributes) ⇒ Object

Internal - Configure virtual attributes for this server. If the option is ‘true`, we’ll use the default virual attributes. If it’s a Hash we’ll map the attributes in the hash.

attributes: is the option set when Ldap is initialized.

Returns a VirtualAttributes.



259
260
261
262
263
264
265
266
267
# File 'lib/github/ldap.rb', line 259

def configure_virtual_attributes(attributes)
  @virtual_attributes = if attributes == true
    VirtualAttributes.new(true)
  elsif attributes.is_a?(Hash)
    VirtualAttributes.new(true, attributes)
  else
    VirtualAttributes.new(false)
  end
end

#domain(base_name) ⇒ Object

Public - Creates a new domain object to perform operations

base_name: is the dn of the base root.

Returns a new Domain object.



164
165
166
# File 'lib/github/ldap.rb', line 164

def domain(base_name)
  Domain.new(self, base_name, @uid)
end

#group(base_name) ⇒ Object

Public - Creates a new group object to perform operations

base_name: is the dn of the base root.

Returns a new Group object. Returns nil if the dn is not in the server.



174
175
176
177
178
179
# File 'lib/github/ldap.rb', line 174

def group(base_name)
  entry = domain(base_name).bind
  return unless entry

  load_group(entry)
end

#load_group(group_entry) ⇒ Object

Public - Create a new group object based on a Net::LDAP::Entry.

group_entry: is a Net::LDAP::Entry.

Returns a Group, PosixGroup or VirtualGroup object.



186
187
188
189
190
191
192
193
194
# File 'lib/github/ldap.rb', line 186

def load_group(group_entry)
  if @virtual_attributes.enabled?
    VirtualGroup.new(self, group_entry)
  elsif posix_support_enabled? && PosixGroup.valid?(group_entry)
    PosixGroup.new(self, group_entry)
  else
    Group.new(self, group_entry)
  end
end

#posix_support_enabled?Boolean

Public - Whether membership checks should include posixGroup filter conditions on ‘memberUid`. Configurable since some LDAP servers don’t handle unsupported attribute queries gracefully.

Enable by passing :posix_support => true.

Returns true, false, or nil (assumed false).

Returns:

  • (Boolean)


145
146
147
# File 'lib/github/ldap.rb', line 145

def posix_support_enabled?
  @posix_support
end

#recursive_group_search_fallback?Boolean

Public - Whether membership checks should recurse into nested groups when virtual attributes aren’t enabled. The fallback search has poor performance characteristics in some cases, in which case this should be disabled by passing :recursive_group_search_fallback => false.

Returns true or false.

Returns:

  • (Boolean)


134
135
136
# File 'lib/github/ldap.rb', line 134

def recursive_group_search_fallback?
  @recursive_group_search_fallback
end

#search(options, &block) ⇒ Object

Public - Search entries in the ldap server.

options: is a hash with the same options that Net::LDAP::Connection#search supports. block: is an optional block to pass to the search.

Returns an Array of Net::LDAP::Entry.



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/github/ldap.rb', line 202

def search(options, &block)
  instrument "search.github_ldap", options.dup do |payload|
    result =
      if options[:base]
        @connection.search(options, &block)
      else
        search_domains.each_with_object([]) do |base, result|
          rs = @connection.search(options.merge(:base => base), &block)
          result.concat Array(rs) unless rs == false
        end
      end

    return [] if result == false
    Array(result)
  end
end

#test_connectionObject

Public - Utility method to check if the connection with the server can be stablished. It tries to bind with the ldap auth default configuration.

Returns an OpenStruct with ‘code` and `message`. If `code` is 0, the operation succeeded and there is no message.



154
155
156
157
# File 'lib/github/ldap.rb', line 154

def test_connection
  @connection.bind
  last_operation_result
end