github-ldap 1.10.0 → 1.10.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6cfd6c19832797f03361e64345fa4b2217d33f88
4
- data.tar.gz: 09dba1eff64b8c5c06053f28a5b48bfdc016362c
3
+ metadata.gz: 8ef4b9c90ff1e809834fe933d93ba5679dd19a1a
4
+ data.tar.gz: 107f31fc7a1b7d30de40ab06976ff2f4ae291adf
5
5
  SHA512:
6
- metadata.gz: e61404084c1b92253ad9564b54a1b4d5f722b809e3f911557695b55d0c93c04d05a8f9a5483eff6b6d543fef9d8fb1990ddcb9e63a9783cfdbfb935609900508
7
- data.tar.gz: ab29b6ea81ebadb2c903b38ae56df15cb0d682506d3e47866a1a1e5a9a0dee1c9b69d02e1550a38b5d0ef081d36221b83d8bed40604bc07d280f9fa38ff0bd5f
6
+ metadata.gz: 606264ea40bdb144f2b13b2e15cf8bc07af9c291730efe9b0b3c4c0f39b318317e580937a7f23e21514553209e3e24591628900276347fed2d7a395b75bd39fa
7
+ data.tar.gz: 95c0822fdaf65f6c1c426ef2a2972c64e61a7e6a2655285e84ae4fd7cd43f795b5d5f1e8cf0929a7ae8e4be35a119170b814e0c01f892394f75e83e14413984d
@@ -1,12 +1,18 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
3
+ - 2.0.0
4
4
  - 2.1.0
5
5
 
6
6
  env:
7
7
  - TESTENV=openldap
8
8
  - TESTENV=apacheds
9
9
 
10
+ # https://docs.travis-ci.com/user/hosts/
11
+ addons:
12
+ hosts:
13
+ - ad1.ghe.dev
14
+ - ad2.ghe.dev
15
+
10
16
  install:
11
17
  - if [ "$TESTENV" = "openldap" ]; then ./script/install-openldap; fi
12
18
  - bundle install
@@ -1,5 +1,9 @@
1
1
  # CHANGELOG
2
2
 
3
+ # v1.10.1
4
+
5
+ * Bump net-ldap to 0.16.0
6
+
3
7
  # v1.10.0
4
8
 
5
9
  * Bump net-ldap to 0.15.0 [#92](https://github.com/github/github-ldap/pull/92)
data/Gemfile CHANGED
@@ -6,3 +6,7 @@ gemspec
6
6
  group :test, :development do
7
7
  gem "byebug", :platforms => [:mri_20, :mri_21]
8
8
  end
9
+
10
+ group :test do
11
+ gem "mocha"
12
+ end
data/README.md CHANGED
@@ -28,6 +28,7 @@ There are a few configuration options required to use this adapter:
28
28
 
29
29
  * host: is the host address where the ldap server lives.
30
30
  * port: is the port where the ldap server lives.
31
+ * hosts: (optional) an enumerable of pairs of hosts and corresponding ports with which to attempt opening connections (default [[host, port]]). Overrides host and port if set.
31
32
  * encryption: is the encryption protocol, disabled by default. The valid options are `ssl` and `tls`.
32
33
  * uid: is the field name in the ldap server used to authenticate your users, in ActiveDirectory this is `sAMAccountName`.
33
34
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "github-ldap"
5
- spec.version = "1.10.0"
5
+ spec.version = "1.10.1"
6
6
  spec.authors = ["David Calavera", "Matt Todd"]
7
7
  spec.email = ["david.calavera@gmail.com", "chiology@gmail.com"]
8
8
  spec.description = %q{LDAP authentication for humans}
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
16
  spec.require_paths = ["lib"]
17
17
 
18
- spec.add_dependency 'net-ldap', '~> 0.15.0'
18
+ spec.add_dependency 'net-ldap', '~> 0.16.0'
19
19
 
20
20
  spec.add_development_dependency "bundler", "~> 1.3"
21
21
  spec.add_development_dependency 'ladle'
@@ -10,6 +10,11 @@ require 'github/ldap/virtual_attributes'
10
10
  require 'github/ldap/instrumentation'
11
11
  require 'github/ldap/member_search'
12
12
  require 'github/ldap/membership_validators'
13
+ require 'github/ldap/user_search/default'
14
+ require 'github/ldap/user_search/active_directory'
15
+ require 'github/ldap/connection_cache'
16
+ require 'github/ldap/referral_chaser'
17
+ require 'github/ldap/url'
13
18
 
14
19
  module GitHub
15
20
  class Ldap
@@ -38,11 +43,17 @@ module GitHub
38
43
  #
39
44
  # Returns the return value of the block.
40
45
  def_delegator :@connection, :open
46
+ def_delegator :@connection, :host
41
47
 
42
48
  attr_reader :uid, :search_domains, :virtual_attributes,
43
49
  :membership_validator,
44
50
  :member_search_strategy,
45
- :instrumentation_service
51
+ :instrumentation_service,
52
+ :user_search_strategy,
53
+ :connection,
54
+ :admin_user,
55
+ :admin_password,
56
+ :port
46
57
 
47
58
  # Build a new GitHub::Ldap instance
48
59
  #
@@ -50,6 +61,9 @@ module GitHub
50
61
  #
51
62
  # host: required string ldap server host address
52
63
  # port: required string or number ldap server port
64
+ # hosts: an enumerable of pairs of hosts and corresponding ports with
65
+ # which to attempt opening connections (default [[host, port]]). Overrides
66
+ # host and port if set.
53
67
  # encryption: optional string. `ssl` or `tls`. nil by default
54
68
  # admin_user: optional string ldap administrator user dn for authentication
55
69
  # admin_password: optional string ldap administrator user password
@@ -69,9 +83,15 @@ module GitHub
69
83
  def initialize(options = {})
70
84
  @uid = options[:uid] || "sAMAccountName"
71
85
 
86
+ # Keep a reference to these as default auth for a Global Catalog if needed
87
+ @admin_user = options[:admin_user]
88
+ @admin_password = options[:admin_password]
89
+ @port = options[:port]
90
+
72
91
  @connection = Net::LDAP.new({
73
92
  host: options[:host],
74
93
  port: options[:port],
94
+ hosts: options[:hosts],
75
95
  instrumentation_service: options[:instrumentation_service]
76
96
  })
77
97
 
@@ -98,6 +118,9 @@ module GitHub
98
118
  # configure both the membership validator and the member search strategies
99
119
  configure_search_strategy(options[:search_strategy])
100
120
 
121
+ # configure the strategy used by Domain#user? to look up a user entry for login
122
+ configure_user_search_strategy(options[:user_search_strategy])
123
+
101
124
  # enables instrumenting queries
102
125
  @instrumentation_service = options[:instrumentation_service]
103
126
  end
@@ -281,6 +304,28 @@ module GitHub
281
304
  end
282
305
  end
283
306
 
307
+ # Internal: Set the user search strategy that will be used by
308
+ # Domain#user?.
309
+ #
310
+ # strategy - Can be either 'default' or 'global_catalog'.
311
+ # 'default' strategy will search the configured
312
+ # domain controller with a search base relative
313
+ # to the controller's domain context.
314
+ # 'global_catalog' will search the entire forest
315
+ # using Active Directory's Global Catalog
316
+ # functionality.
317
+ def configure_user_search_strategy(strategy)
318
+ @user_search_strategy =
319
+ case strategy.to_s
320
+ when "default"
321
+ GitHub::Ldap::UserSearch::Default.new(self)
322
+ when "global_catalog"
323
+ GitHub::Ldap::UserSearch::ActiveDirectory.new(self)
324
+ else
325
+ GitHub::Ldap::UserSearch::Default.new(self)
326
+ end
327
+ end
328
+
284
329
  # Internal: Configure the member search strategy.
285
330
  #
286
331
  #
@@ -0,0 +1,26 @@
1
+ module GitHub
2
+ class Ldap
3
+
4
+ # A simple cache of GitHub::Ldap objects to prevent creating multiple
5
+ # instances of connections that point to the same URI/host.
6
+ class ConnectionCache
7
+
8
+ # Public - Create or return cached instance of GitHub::Ldap created with options,
9
+ # where the cache key is the value of options[:host].
10
+ #
11
+ # options - Initialization attributes suitable for creating a new connection with
12
+ # GitHub::Ldap.new(options)
13
+ #
14
+ # Returns true or false.
15
+ def self.get_connection(options={})
16
+ @cache ||= self.new
17
+ @cache.get_connection(options)
18
+ end
19
+
20
+ def get_connection(options)
21
+ @connections ||= {}
22
+ @connections[options[:host]] ||= GitHub::Ldap.new(options)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -115,10 +115,7 @@ module GitHub
115
115
  # Returns the user if the login matches any `uid`.
116
116
  # Returns nil if there are no matches.
117
117
  def user?(login, search_options = {})
118
- options = search_options.merge \
119
- filter: login_filter(@uid, login),
120
- size: 1
121
- search(options).first
118
+ @ldap.user_search_strategy.perform(login, @base_name, @uid, search_options).first
122
119
  end
123
120
 
124
121
  # Check if a user can be bound with a password.
@@ -24,15 +24,23 @@ module GitHub
24
24
  # Sets the entry to the base and scopes the search to the base,
25
25
  # according to the source documentation, found here:
26
26
  # http://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx
27
- matched = ldap.search \
27
+ #
28
+ # Use ReferralChaser to chase any potential referrals for an entry that may be owned by a different
29
+ # domain controller.
30
+ matched = referral_chaser.search \
28
31
  filter: membership_in_chain_filter(entry),
29
32
  base: entry.dn,
30
33
  scope: Net::LDAP::SearchScope_BaseObject,
34
+ return_referrals: true,
31
35
  attributes: ATTRS
32
36
 
33
37
  # membership validated if entry was matched and returned as a result
34
38
  # Active Directory DNs are case-insensitive
35
- matched.map { |m| m.dn.downcase }.include?(entry.dn.downcase)
39
+ Array(matched).map { |m| m.dn.downcase }.include?(entry.dn.downcase)
40
+ end
41
+
42
+ def referral_chaser
43
+ @referral_chaser ||= GitHub::Ldap::ReferralChaser.new(@ldap)
36
44
  end
37
45
 
38
46
  # Internal: Constructs a membership filter using the "in chain"
@@ -0,0 +1,98 @@
1
+ module GitHub
2
+ class Ldap
3
+
4
+ # This class adds referral chasing capability to a GitHub::Ldap connection.
5
+ #
6
+ # See: https://technet.microsoft.com/en-us/library/cc978014.aspx
7
+ # http://www.umich.edu/~dirsvcs/ldap/doc/other/ldap-ref.html
8
+ #
9
+ class ReferralChaser
10
+
11
+ # Public - Creates a ReferralChaser that decorates an instance of GitHub::Ldap
12
+ # with additional functionality to the #search method, allowing it to chase
13
+ # any referral entries and aggregate the results into a single response.
14
+ #
15
+ # connection - The instance of GitHub::Ldap to use for searching. Will use
16
+ # the connection's authentication, (admin_user and admin_password) as credentials
17
+ # for connecting to referred domain controllers.
18
+ def initialize(connection)
19
+ @connection = connection
20
+ @admin_user = connection.admin_user
21
+ @admin_password = connection.admin_password
22
+ @port = connection.port
23
+ end
24
+
25
+ # Public - Search the domain controller represented by this instance's connection.
26
+ # If a referral is returned, search only one of the domain controllers indicated
27
+ # by the referral entries, per RFC 4511 (https://tools.ietf.org/html/rfc4511):
28
+ #
29
+ # "If the client wishes to progress the operation, it contacts one of
30
+ # the supported services found in the referral. If multiple URIs are
31
+ # present, the client assumes that any supported URI may be used to
32
+ # progress the operation."
33
+ #
34
+ # options - is a hash with the same options that Net::LDAP::Connection#search supports.
35
+ # Referral searches will use the given options, but will replace options[:base]
36
+ # with the referral URL's base search dn.
37
+ #
38
+ # Does not take a block argument as GitHub::Ldap and Net::LDAP::Connection#search do.
39
+ #
40
+ # Will not recursively follow any subsequent referrals.
41
+ #
42
+ # Returns an Array of Net::LDAP::Entry.
43
+ def search(options)
44
+ search_results = []
45
+ referral_entries = []
46
+
47
+ search_results = connection.search(options) do |entry|
48
+ if entry && entry[:search_referrals]
49
+ referral_entries << entry
50
+ end
51
+ end
52
+
53
+ unless referral_entries.empty?
54
+ entry = referral_entries.first
55
+ referral_string = entry[:search_referrals].first
56
+ if GitHub::Ldap::URL.valid?(referral_string)
57
+ referral = Referral.new(referral_string, admin_user, admin_password, port)
58
+ search_results = referral.search(options)
59
+ end
60
+ end
61
+
62
+ Array(search_results)
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :connection, :admin_user, :admin_password, :port
68
+
69
+ # Represents a referral entry from an LDAP search result. Constructs a corresponding
70
+ # GitHub::Ldap object from the paramaters on the referral_url and provides a #search
71
+ # method to continue the search on the referred domain.
72
+ class Referral
73
+ def initialize(referral_url, admin_user, admin_password, port=nil)
74
+ url = GitHub::Ldap::URL.new(referral_url)
75
+ @search_base = url.dn
76
+
77
+ connection_options = {
78
+ host: url.host,
79
+ port: port || url.port,
80
+ scope: url.scope,
81
+ admin_user: admin_user,
82
+ admin_password: admin_password
83
+ }
84
+
85
+ @connection = GitHub::Ldap::ConnectionCache.get_connection(connection_options)
86
+ end
87
+
88
+ # Search the referred domain controller with options, merging in the referred search
89
+ # base DN onto options[:base].
90
+ def search(options)
91
+ connection.search(options.merge(base: search_base))
92
+ end
93
+
94
+ attr_reader :search_base, :connection
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,87 @@
1
+ module GitHub
2
+ class Ldap
3
+
4
+ # This class represents an LDAP URL
5
+ #
6
+ # See: https://tools.ietf.org/html/rfc4516#section-2
7
+ # https://docs.oracle.com/cd/E19957-01/817-6707/urls.html
8
+ #
9
+ class URL
10
+ extend Forwardable
11
+ SCOPES = {
12
+ "base" => Net::LDAP::SearchScope_BaseObject,
13
+ "one" => Net::LDAP::SearchScope_SingleLevel,
14
+ "sub" => Net::LDAP::SearchScope_WholeSubtree
15
+ }
16
+ SCOPES.default = Net::LDAP::SearchScope_BaseObject
17
+
18
+ attr_reader :dn, :attributes, :scope, :filter
19
+
20
+ def_delegators :@uri, :port, :host, :scheme
21
+
22
+ # Public - Creates a new GitHub::Ldap::URL object with :port, :host and :scheme
23
+ # delegated to a URI object parsed from url_string, and then parses the
24
+ # query params according to the LDAP specification.
25
+ #
26
+ # url_string - An LDAP URL string.
27
+ # returns - a GitHub::Ldap::URL with the following attributes:
28
+ # host - Name or IP of the LDAP server.
29
+ # port - The given port, defaults to 389.
30
+ # dn - The base search DN.
31
+ # attributes - The comma-delimited list of attributes to be returned.
32
+ # scope - The scope of the search.
33
+ # filter - Search filter to apply to entries within the specified scope of the search.
34
+ #
35
+ # Supported LDAP URL strings look like this, where sections in brackets are optional:
36
+ #
37
+ # ldap[s]://[hostport][/[dn[?[attributes][?[scope][?[filter]]]]]]
38
+ #
39
+ # where:
40
+ #
41
+ # hostport is a host name with an optional ":portnumber"
42
+ # dn is the base DN to be used for an LDAP search operation
43
+ # attributes is a comma separated list of attributes to be retrieved
44
+ # scope is one of these three strings: base one sub (default=base)
45
+ # filter is LDAP search filter as used in a call to ldap_search
46
+ #
47
+ # For example:
48
+ #
49
+ # ldap://dc4.ghe.local:456/CN=Maggie,DC=dc4,DC=ghe,DC=local?cn,mail?base?(cn=Charlie)
50
+ #
51
+ def initialize(url_string)
52
+ if !self.class.valid?(url_string)
53
+ raise InvalidLdapURLException.new("Invalid LDAP URL: #{url_string}")
54
+ end
55
+ @uri = URI(url_string)
56
+ @dn = URI.unescape(@uri.path.sub(/^\//, ""))
57
+ if @uri.query
58
+ @attributes, @scope, @filter = @uri.query.split("?")
59
+ end
60
+ end
61
+
62
+ def self.valid?(url_string)
63
+ url_string =~ URI::regexp && ["ldap", "ldaps"].include?(URI(url_string).scheme)
64
+ end
65
+
66
+ # Maps the returned scope value from the URL to one of Net::LDAP::Scopes
67
+ #
68
+ # The URL scope value can be one of:
69
+ # "base" - retrieves information only about the DN (base_dn) specified.
70
+ # "one" - retrieves information about entries one level below the DN (base_dn) specified. The base entry is not included in this scope.
71
+ # "sub" - retrieves information about entries at all levels below the DN (base_dn) specified. The base entry is included in this scope.
72
+ #
73
+ # Which will map to one of the following Net::LDAP::Scopes:
74
+ # SearchScope_BaseObject = 0
75
+ # SearchScope_SingleLevel = 1
76
+ # SearchScope_WholeSubtree = 2
77
+ #
78
+ # If no scope or an invalid scope is given, defaults to SearchScope_BaseObject
79
+ def net_ldap_scope
80
+ Net::LDAP::SearchScopes[SCOPES[scope]]
81
+ end
82
+
83
+ class InvalidLdapURLException < Exception; end
84
+ end
85
+ end
86
+ end
87
+
@@ -0,0 +1,51 @@
1
+ module GitHub
2
+ class Ldap
3
+ module UserSearch
4
+ class ActiveDirectory < Default
5
+
6
+ private
7
+
8
+ # Private - Overridden from base class to set the base to "", and use the
9
+ # Global Catalog to perform the user search.
10
+ def search(search_options)
11
+ Array(global_catalog_connection.search(search_options.merge(options)))
12
+ end
13
+
14
+ def global_catalog_connection
15
+ GlobalCatalog.connection(ldap)
16
+ end
17
+
18
+ # When doing a global search for a user's DN, set the search base to blank
19
+ def options
20
+ super.merge(base: "")
21
+ end
22
+ end
23
+
24
+ class GlobalCatalog < Net::LDAP
25
+ STANDARD_GC_PORT = 3268
26
+ LDAPS_GC_PORT = 3269
27
+
28
+ # Returns a connection to the Active Directory Global Catalog
29
+ #
30
+ # See: https://technet.microsoft.com/en-us/library/cc728188(v=ws.10).aspx
31
+ #
32
+ def self.connection(ldap)
33
+ @global_catalog_instance ||= begin
34
+ netldap = ldap.connection
35
+ # This is ugly, but Net::LDAP doesn't expose encryption or auth
36
+ encryption = netldap.instance_variable_get(:@encryption)
37
+ auth = netldap.instance_variable_get(:@auth)
38
+
39
+ new({
40
+ host: ldap.host,
41
+ instrumentation_service: ldap.instrumentation_service,
42
+ port: encryption ? LDAPS_GC_PORT : STANDARD_GC_PORT,
43
+ auth: auth,
44
+ encryption: encryption
45
+ })
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,40 @@
1
+ module GitHub
2
+ class Ldap
3
+ module UserSearch
4
+ # The default user search strategy, mainly for allowing Domain#user? to
5
+ # search for a user on the configured domain controller, or use the Global
6
+ # Catalog to search across the entire Active Directory forest.
7
+ class Default
8
+ include Filter
9
+
10
+ def initialize(ldap)
11
+ @ldap = ldap
12
+ @options = {
13
+ :attributes => [],
14
+ :paged_searches_supported => true,
15
+ :size => 1
16
+ }
17
+ end
18
+
19
+ # Performs a normal search on the configured domain controller
20
+ # using the default base DN, uid, search_options
21
+ def perform(login, base_name, uid, search_options)
22
+ search_options[:filter] = login_filter(uid, login)
23
+ search_options[:base] = base_name
24
+ search(options.merge(search_options))
25
+ end
26
+
27
+ # The default search. This can be overridden by a child class
28
+ # like GitHub::Ldap::UserSearch::ActiveDirectory to change the
29
+ # scope of the search.
30
+ def search(options)
31
+ ldap.search(options)
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :options, :ldap
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,18 @@
1
+ require_relative 'test_helper'
2
+
3
+ class GitHubLdapConnectionCacheTestCases < GitHub::Ldap::Test
4
+
5
+ def test_returns_cached_connection
6
+ conn1 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad1.ghe.dev"))
7
+ conn2 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad1.ghe.dev"))
8
+ assert_equal conn1.object_id, conn2.object_id
9
+ end
10
+
11
+ def test_creates_new_connections_per_host
12
+ conn1 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad1.ghe.dev"))
13
+ conn2 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad2.ghe.dev"))
14
+ conn3 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad2.ghe.dev"))
15
+ refute_equal conn1.object_id, conn2.object_id
16
+ assert_equal conn2.object_id, conn3.object_id
17
+ end
18
+ end
@@ -140,6 +140,15 @@ module GitHubLdapDomainTestCases
140
140
  assert user = @domain.user?('user1')
141
141
  refute @domain.auth(user, 'foo'), 'Expected user not not bind'
142
142
  end
143
+
144
+ def test_user_search_returns_first_entry
145
+ entry = mock("Net::Ldap::Entry")
146
+ search_strategy = mock("GitHub::Ldap::UserSearch::Default")
147
+ search_strategy.stubs(:perform).returns([entry])
148
+ @ldap.expects(:user_search_strategy).returns(search_strategy)
149
+ user = @domain.user?('user1', :attributes => [:cn])
150
+ assert_equal entry, user
151
+ end
143
152
  end
144
153
 
145
154
  class GitHubLdapDomainTest < GitHub::Ldap::Test
@@ -9,6 +9,21 @@ module GitHubLdapTestCases
9
9
  assert @ldap.test_connection, "Ldap connection expected to succeed"
10
10
  end
11
11
 
12
+ def test_connection_with_list_of_hosts_with_one_valid_host
13
+ ldap = GitHub::Ldap.new(options.merge(hosts: [["localhost", options[:port]]]))
14
+ assert ldap.test_connection, "Ldap connection expected to succeed"
15
+ end
16
+
17
+ def test_connection_with_list_of_hosts_with_first_valid
18
+ ldap = GitHub::Ldap.new(options.merge(hosts: [["localhost", options[:port]], ["invalid.local", options[:port]]]))
19
+ assert ldap.test_connection, "Ldap connection expected to succeed"
20
+ end
21
+
22
+ def test_connection_with_list_of_hosts_with_first_invalid
23
+ ldap = GitHub::Ldap.new(options.merge(hosts: [["invalid.local", options[:port]], ["localhost", options[:port]]]))
24
+ assert ldap.test_connection, "Ldap connection expected to succeed"
25
+ end
26
+
12
27
  def test_simple_tls
13
28
  assert_equal :simple_tls, @ldap.check_encryption(:ssl)
14
29
  assert_equal :simple_tls, @ldap.check_encryption('SSL')
@@ -114,6 +129,17 @@ module GitHubLdapTestCases
114
129
  assert_equal GitHub::Ldap::MemberSearch::Recursive, @ldap.member_search_strategy
115
130
  end
116
131
 
132
+ def test_user_search_strategy_global_catalog_when_configured
133
+ @ldap.configure_user_search_strategy("global_catalog")
134
+ assert_kind_of GitHub::Ldap::UserSearch::ActiveDirectory, @ldap.user_search_strategy
135
+ end
136
+
137
+ def test_user_search_strategy_default_when_configured
138
+ @ldap.configure_user_search_strategy("default")
139
+ refute_kind_of GitHub::Ldap::UserSearch::ActiveDirectory, @ldap.user_search_strategy
140
+ assert_kind_of GitHub::Ldap::UserSearch::Default, @ldap.user_search_strategy
141
+ end
142
+
117
143
  def test_capabilities
118
144
  assert_kind_of Net::LDAP::Entry, @ldap.capabilities
119
145
  end
@@ -0,0 +1,102 @@
1
+ require_relative 'test_helper'
2
+
3
+ class GitHubLdapReferralChaserTestCases < GitHub::Ldap::Test
4
+
5
+ def setup
6
+ @ldap = GitHub::Ldap.new(options)
7
+ @chaser = GitHub::Ldap::ReferralChaser.new(@ldap)
8
+ end
9
+
10
+ def test_creates_referral_with_connection_credentials
11
+ @ldap.expects(:search).yields({ search_referrals: ["ldap://dc1.ghe.local/"]}).returns([])
12
+
13
+ referral = mock("GitHub::Ldap::ReferralChaser::Referral")
14
+ referral.stubs(:search).returns([])
15
+
16
+ GitHub::Ldap::ReferralChaser::Referral.expects(:new)
17
+ .with("ldap://dc1.ghe.local/", "uid=admin,dc=github,dc=com", "passworD1", options[:port])
18
+ .returns(referral)
19
+
20
+ @chaser.search({})
21
+ end
22
+
23
+ def test_creates_referral_with_default_port
24
+ @ldap.expects(:search).yields({
25
+ search_referrals: ["ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"]
26
+ }).returns([])
27
+
28
+ stub_referral_connection = mock("GitHub::Ldap")
29
+ stub_referral_connection.stubs(:search).returns([])
30
+ GitHub::Ldap::ConnectionCache.expects(:get_connection).with(has_entry(port: options[:port])).returns(stub_referral_connection)
31
+ chaser = GitHub::Ldap::ReferralChaser.new(@ldap)
32
+ chaser.search({})
33
+ end
34
+
35
+ def test_creates_referral_for_first_referral_string
36
+ @ldap.expects(:search).multiple_yields([
37
+ { search_referrals:
38
+ ["ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local",
39
+ "ldap://dc2.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"]
40
+ }
41
+ ],[
42
+ { search_referrals:
43
+ ["ldap://dc3.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local",
44
+ "ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"]
45
+ }
46
+ ]).returns([])
47
+
48
+ referral = mock("GitHub::Ldap::ReferralChaser::Referral")
49
+ referral.stubs(:search).returns([])
50
+
51
+ GitHub::Ldap::ReferralChaser::Referral.expects(:new)
52
+ .with(
53
+ "ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local",
54
+ "uid=admin,dc=github,dc=com",
55
+ "passworD1",
56
+ options[:port])
57
+ .returns(referral)
58
+
59
+ @chaser.search({})
60
+ end
61
+
62
+ def test_returns_referral_search_results
63
+ @ldap.expects(:search).multiple_yields([
64
+ { search_referrals:
65
+ ["ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local",
66
+ "ldap://dc2.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"]
67
+ }
68
+ ],[
69
+ { search_referrals:
70
+ ["ldap://dc3.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local",
71
+ "ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"]
72
+ }
73
+ ]).returns([])
74
+
75
+ referral = mock("GitHub::Ldap::ReferralChaser::Referral")
76
+ referral.expects(:search).returns(["result", "result"])
77
+
78
+ GitHub::Ldap::ReferralChaser::Referral.expects(:new).returns(referral)
79
+
80
+ results = @chaser.search({})
81
+ assert_equal(["result", "result"], results)
82
+ end
83
+
84
+ def test_handle_blank_url_string_in_referral
85
+ @ldap.expects(:search).yields({ search_referrals: [""] })
86
+
87
+ results = @chaser.search({})
88
+ assert_equal([], results)
89
+ end
90
+
91
+ def test_returns_referral_search_results
92
+ @ldap.expects(:search).yields({ foo: ["not a referral"] })
93
+
94
+ GitHub::Ldap::ReferralChaser::Referral.expects(:new).never
95
+ results = @chaser.search({})
96
+ end
97
+
98
+ def test_referral_should_use_host_from_referral_string
99
+ GitHub::Ldap::ConnectionCache.expects(:get_connection).with(has_entry(host: "dc4.ghe.local"))
100
+ GitHub::Ldap::ReferralChaser::Referral.new("ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", "", "")
101
+ end
102
+ end
@@ -13,6 +13,8 @@ require 'github/ldap/server'
13
13
  require 'minitest/mock'
14
14
  require 'minitest/autorun'
15
15
 
16
+ require 'mocha/mini_test'
17
+
16
18
  if ENV.fetch('TESTENV', "apacheds") == "apacheds"
17
19
  # Make sure we clean up running test server
18
20
  # NOTE: We need to do this manually since its internal `at_exit` hook
@@ -0,0 +1,85 @@
1
+ require_relative 'test_helper'
2
+
3
+ class GitHubLdapURLTestCases < GitHub::Ldap::Test
4
+
5
+ def setup
6
+ @url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local:123/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local?cn,mail,telephoneNumber?base?(cn=Charlie)")
7
+ end
8
+
9
+ def test_host
10
+ assert_equal "dc4.ghe.local", @url.host
11
+ end
12
+
13
+ def test_port
14
+ assert_equal 123, @url.port
15
+ end
16
+
17
+ def test_scheme
18
+ assert_equal "ldap", @url.scheme
19
+ end
20
+
21
+ def test_default_port
22
+ url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local?attributes?scope?filter")
23
+ assert_equal 389, url.port
24
+ end
25
+
26
+ def test_simple_url
27
+ url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local")
28
+ assert_equal 389, url.port
29
+ assert_equal "dc4.ghe.local", url.host
30
+ assert_equal "ldap", url.scheme
31
+ assert_equal "", url.dn
32
+ assert_equal nil, url.attributes
33
+ assert_equal nil, url.filter
34
+ assert_equal nil, url.scope
35
+ end
36
+
37
+ def test_invalid_scheme
38
+ ex = assert_raises(GitHub::Ldap::URL::InvalidLdapURLException) do
39
+ GitHub::Ldap::URL.new("http://dc4.ghe.local")
40
+ end
41
+ assert_equal("Invalid LDAP URL: http://dc4.ghe.local", ex.message)
42
+ end
43
+
44
+ def test_invalid_url
45
+ ex = assert_raises(GitHub::Ldap::URL::InvalidLdapURLException) do
46
+ GitHub::Ldap::URL.new("not a url")
47
+ end
48
+ assert_equal("Invalid LDAP URL: not a url", ex.message)
49
+ end
50
+
51
+ def test_parse_dn
52
+ assert_equal "CN=Maggie Mae,CN=Users,DC=dc4,DC=ghe,DC=local", @url.dn
53
+ end
54
+
55
+ def test_parse_attributes
56
+ assert_equal "cn,mail,telephoneNumber", @url.attributes
57
+ end
58
+
59
+ def test_parse_filter
60
+ assert_equal "(cn=Charlie)", @url.filter
61
+ end
62
+
63
+ def test_parse_scope
64
+ assert_equal "base", @url.scope
65
+ end
66
+
67
+ def test_default_scope
68
+ url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/base_dn?cn=joe??filter")
69
+ assert_equal "", url.scope
70
+ end
71
+
72
+ def test_net_ldap_scopes
73
+ sub_scope_url = GitHub::Ldap::URL.new("ldap://ghe.local/base_dn?cn=joe?sub?filter")
74
+ one_scope_url = GitHub::Ldap::URL.new("ldap://ghe.local/base_dn?cn=joe?one?filter")
75
+ base_scope_url = GitHub::Ldap::URL.new("ldap://ghe.local/base_dn?cn=joe?base?filter")
76
+ default_scope_url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/base_dn?cn=joe??filter")
77
+ invalid_scope_url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/base_dn?cn=joe?invalid?filter")
78
+
79
+ assert_equal Net::LDAP::SearchScope_BaseObject, base_scope_url.net_ldap_scope
80
+ assert_equal Net::LDAP::SearchScope_SingleLevel, one_scope_url.net_ldap_scope
81
+ assert_equal Net::LDAP::SearchScope_WholeSubtree, sub_scope_url.net_ldap_scope
82
+ assert_equal Net::LDAP::SearchScope_BaseObject, default_scope_url.net_ldap_scope
83
+ assert_equal Net::LDAP::SearchScope_BaseObject, invalid_scope_url.net_ldap_scope
84
+ end
85
+ end
@@ -0,0 +1,53 @@
1
+ require_relative '../test_helper'
2
+
3
+ class GitHubLdapActiveDirectoryUserSearchTests < GitHub::Ldap::Test
4
+
5
+ def test_global_catalog_returns_empty_array_for_no_results
6
+ ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev'))
7
+ ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(ldap)
8
+
9
+ mock_global_catalog_connection = mock("GitHub::Ldap::UserSearch::GlobalCatalog")
10
+ mock_global_catalog_connection.expects(:search).returns(nil)
11
+ ad_user_search.expects(:global_catalog_connection).returns(mock_global_catalog_connection)
12
+ results = ad_user_search.perform("login", "CN=Joe", "uid", {})
13
+ assert_equal [], results
14
+ end
15
+
16
+ def test_global_catalog_returns_array_of_results
17
+ ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev'))
18
+ ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(ldap)
19
+
20
+ mock_global_catalog_connection = mock("GitHub::Ldap::UserSearch::GlobalCatalog")
21
+ stub_entry = mock("Net::LDAP::Entry")
22
+
23
+ mock_global_catalog_connection.expects(:search).returns(stub_entry)
24
+ ad_user_search.expects(:global_catalog_connection).returns(mock_global_catalog_connection)
25
+
26
+ results = ad_user_search.perform("login", "CN=Joe", "uid", {})
27
+ assert_equal [stub_entry], results
28
+ end
29
+
30
+ def test_searches_with_empty_base_dn
31
+ ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev'))
32
+ ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(ldap)
33
+
34
+ mock_global_catalog_connection = mock("GitHub::Ldap::UserSearch::GlobalCatalog")
35
+ mock_global_catalog_connection.expects(:search).with(has_entry(:base => ""))
36
+ ad_user_search.expects(:global_catalog_connection).returns(mock_global_catalog_connection)
37
+ ad_user_search.perform("login", "CN=Joe", "uid", {})
38
+ end
39
+
40
+ def test_global_catalog_default_settings
41
+ ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev'))
42
+ global_catalog = GitHub::Ldap::UserSearch::GlobalCatalog.connection(ldap)
43
+ instrumentation_service = global_catalog.instance_variable_get(:@instrumentation_service)
44
+
45
+ auth = global_catalog.instance_variable_get(:@auth)
46
+ assert_equal :simple, auth[:method]
47
+ assert_equal "uid=admin,dc=github,dc=com", auth[:username]
48
+ assert_equal "passworD1", auth[:password]
49
+ assert_equal "ghe.dev", global_catalog.host
50
+ assert_equal 3268, global_catalog.port
51
+ assert_equal "MockInstrumentationService", instrumentation_service.class.name
52
+ end
53
+ end
@@ -0,0 +1,19 @@
1
+ require_relative '../test_helper'
2
+
3
+ class GitHubLdapActiveDirectoryUserSearchTests < GitHub::Ldap::Test
4
+ def setup
5
+ @ldap = GitHub::Ldap.new(options)
6
+ @default_user_search = GitHub::Ldap::UserSearch::Default.new(@ldap)
7
+ end
8
+
9
+ def test_default_search_options
10
+ @ldap.expects(:search).with(has_entries(
11
+ attributes: [],
12
+ size: 1,
13
+ paged_searches_supported: true,
14
+ base: "CN=HI,CN=McDunnough",
15
+ filter: kind_of(Net::LDAP::Filter)
16
+ ))
17
+ @default_user_search.perform("","CN=HI,CN=McDunnough","",{})
18
+ end
19
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: github-ldap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.0
4
+ version: 1.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Calavera
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-07-13 00:00:00.000000000 Z
12
+ date: 2017-02-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: net-ldap
@@ -17,14 +17,14 @@ dependencies:
17
17
  requirements:
18
18
  - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: 0.15.0
20
+ version: 0.16.0
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: 0.15.0
27
+ version: 0.16.0
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: bundler
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -98,6 +98,7 @@ files:
98
98
  - Rakefile
99
99
  - github-ldap.gemspec
100
100
  - lib/github/ldap.rb
101
+ - lib/github/ldap/connection_cache.rb
101
102
  - lib/github/ldap/domain.rb
102
103
  - lib/github/ldap/filter.rb
103
104
  - lib/github/ldap/fixtures.ldif
@@ -114,7 +115,11 @@ files:
114
115
  - lib/github/ldap/membership_validators/classic.rb
115
116
  - lib/github/ldap/membership_validators/recursive.rb
116
117
  - lib/github/ldap/posix_group.rb
118
+ - lib/github/ldap/referral_chaser.rb
117
119
  - lib/github/ldap/server.rb
120
+ - lib/github/ldap/url.rb
121
+ - lib/github/ldap/user_search/active_directory.rb
122
+ - lib/github/ldap/user_search/default.rb
118
123
  - lib/github/ldap/virtual_attributes.rb
119
124
  - lib/github/ldap/virtual_group.rb
120
125
  - script/changelog
@@ -123,6 +128,7 @@ files:
123
128
  - script/install-openldap
124
129
  - script/package
125
130
  - script/release
131
+ - test/connection_cache_test.rb
126
132
  - test/domain_test.rb
127
133
  - test/filter_test.rb
128
134
  - test/fixtures/common/seed.ldif
@@ -138,6 +144,7 @@ files:
138
144
  - test/membership_validators/classic_test.rb
139
145
  - test/membership_validators/recursive_test.rb
140
146
  - test/posix_group_test.rb
147
+ - test/referral_chaser_test.rb
141
148
  - test/support/vm/activedirectory/.gitignore
142
149
  - test/support/vm/activedirectory/README.md
143
150
  - test/support/vm/activedirectory/env.sh.example
@@ -146,6 +153,9 @@ files:
146
153
  - test/support/vm/openldap/README.md
147
154
  - test/support/vm/openldap/Vagrantfile
148
155
  - test/test_helper.rb
156
+ - test/url_test.rb
157
+ - test/user_search/active_directory_test.rb
158
+ - test/user_search/default_test.rb
149
159
  homepage: https://github.com/github/github-ldap
150
160
  licenses:
151
161
  - MIT
@@ -166,11 +176,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
166
176
  version: '0'
167
177
  requirements: []
168
178
  rubyforge_project:
169
- rubygems_version: 2.5.1
179
+ rubygems_version: 2.5.2
170
180
  signing_key:
171
181
  specification_version: 4
172
182
  summary: LDAP client authentication wrapper without all the boilerplate
173
183
  test_files:
184
+ - test/connection_cache_test.rb
174
185
  - test/domain_test.rb
175
186
  - test/filter_test.rb
176
187
  - test/fixtures/common/seed.ldif
@@ -186,6 +197,7 @@ test_files:
186
197
  - test/membership_validators/classic_test.rb
187
198
  - test/membership_validators/recursive_test.rb
188
199
  - test/posix_group_test.rb
200
+ - test/referral_chaser_test.rb
189
201
  - test/support/vm/activedirectory/.gitignore
190
202
  - test/support/vm/activedirectory/README.md
191
203
  - test/support/vm/activedirectory/env.sh.example
@@ -194,3 +206,6 @@ test_files:
194
206
  - test/support/vm/openldap/README.md
195
207
  - test/support/vm/openldap/Vagrantfile
196
208
  - test/test_helper.rb
209
+ - test/url_test.rb
210
+ - test/user_search/active_directory_test.rb
211
+ - test/user_search/default_test.rb