github-ldap 1.10.0 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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