From 8c945cee422e649ddb1ec1a891b68a65db0a3b00 Mon Sep 17 00:00:00 2001 From: Timm Drevensek Date: Sun, 17 Apr 2016 17:37:31 +0200 Subject: [PATCH 01/26] fix travis to update gems before build --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 09e4709..3583912 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,9 @@ env: - TESTENV=openldap - TESTENV=apacheds +before_install: + - gem update bundler + install: - if [ "$TESTENV" = "openldap" ]; then ./script/install-openldap; fi - bundle install From 0ba39663f6cfe607c61bcccd00e8ad5f42c1fc87 Mon Sep 17 00:00:00 2001 From: Timm Drevensek Date: Sun, 17 Apr 2016 01:52:06 +0200 Subject: [PATCH 02/26] Implement forest search to obtain non universal groups from ad --- lib/github/ldap.rb | 61 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/lib/github/ldap.rb b/lib/github/ldap.rb index 0545247..7784584 100644 --- a/lib/github/ldap.rb +++ b/lib/github/ldap.rb @@ -100,6 +100,9 @@ def initialize(options = {}) # enables instrumenting queries @instrumentation_service = options[:instrumentation_service] + + # active directory forest + @forest = get_domain_forest(options[:search_forest]) end # Public - Whether membership checks should recurse into nested groups when @@ -180,10 +183,10 @@ def search(options, &block) instrument "search.github_ldap", options.dup do |payload| result = if options[:base] - @connection.search(options, &block) + forest_search(options, &block) else search_domains.each_with_object([]) do |base, result| - rs = @connection.search(options.merge(:base => base), &block) + rs = forest_search(options.merge(:base => base), &block) result.concat Array(rs) unless rs == false end end @@ -193,6 +196,26 @@ def search(options, &block) end end + # Internal: Search within a ldap forest + # + # Returns an Array of Net::LDAP::Entry. + def forest_search(options, &block) + instrument "forest_search.github_ldap" do |payload| + result = + if @forest.empty? + @connection.search(options, &block) + else + @forest.each_with_object([]) do |(rootdn, server), res| + if options[:base].end_with?(rootdn) + rs = server.search(options, &block) + res.concat Array(rs) unless rs == false + end + end + end + return result + end + end + # Internal: Searches the host LDAP server's Root DSE for capabilities and # extensions. # @@ -201,7 +224,8 @@ def capabilities @capabilities ||= instrument "capabilities.github_ldap" do |payload| begin - @connection.search_root_dse + rs = @connection.search(:ignore_server_caps => true, :base => "", :scope => Net::LDAP::SearchScope_BaseObject) + (rs and rs.first) rescue Net::LDAP::LdapError => error payload[:error] = error # stubbed result @@ -307,6 +331,37 @@ def configure_member_search_strategy(strategy = nil) end end + # Internal: Queries configuration for available domains + # + # Membership of local or global groups need to be evaluated by contacting referral Donmain Controllers + # + # Returns all Domain Controllers within the forest + def get_domain_forest(search_forest) + instrument "get_domain_forest.github_ldap" do |payload| + + # if we are talking to an active directory + if search_forest and active_directory_capability? and capabilities[:configurationnamingcontext].any? + domains = @connection.search( + base: capabilities[:configurationnamingcontext].first, + search_referrals: true, + filter: Net::LDAP::Filter.eq("nETBIOSName", "*") + ) + return domains.each_with_object({}) do |server, result| + if server[:ncname].any? and server[:dnsroot].any? + result[server[:ncname].first] = Net::LDAP.new({ + host: server[:dnsroot].first, + port: @connection.instance_variable_get(:@encryption)? 636 : 389, + auth: @connection.instance_variable_get(:@auth), + encryption: @connection.instance_variable_get(:@encryption), + instrumentation_service: @connection.instance_variable_get(:@instrumentation_service) + }) + end + end + end + return {} + end + end + # Internal: Detect whether the LDAP host is an ActiveDirectory server. # # See: http://msdn.microsoft.com/en-us/library/cc223359.aspx. From 7c15f6f1aae56c800bb1450cb61bb15b3bf641de Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Fri, 8 Jul 2016 13:33:09 -0500 Subject: [PATCH 03/26] First pass at moving forest searching into AD-only strategy --- lib/github/ldap.rb | 75 ++++++++------------------------ lib/github/ldap/forest_search.rb | 57 ++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 57 deletions(-) create mode 100644 lib/github/ldap/forest_search.rb diff --git a/lib/github/ldap.rb b/lib/github/ldap.rb index 7784584..c1639e7 100644 --- a/lib/github/ldap.rb +++ b/lib/github/ldap.rb @@ -84,6 +84,7 @@ def initialize(options = {}) end configure_virtual_attributes(options[:virtual_attributes]) + configure_entry_search_strategy(options[:search_forest]) # enable fallback recursive group search unless option is false @recursive_group_search_fallback = (options[:recursive_group_search_fallback] != false) @@ -100,9 +101,6 @@ def initialize(options = {}) # enables instrumenting queries @instrumentation_service = options[:instrumentation_service] - - # active directory forest - @forest = get_domain_forest(options[:search_forest]) end # Public - Whether membership checks should recurse into nested groups when @@ -183,10 +181,10 @@ def search(options, &block) instrument "search.github_ldap", options.dup do |payload| result = if options[:base] - forest_search(options, &block) + @entry_search_strategy.search(options, &block) else search_domains.each_with_object([]) do |base, result| - rs = forest_search(options.merge(:base => base), &block) + rs = @entry_search_strategy.search(options.merge(:base => base), &block) result.concat Array(rs) unless rs == false end end @@ -196,26 +194,6 @@ def search(options, &block) end end - # Internal: Search within a ldap forest - # - # Returns an Array of Net::LDAP::Entry. - def forest_search(options, &block) - instrument "forest_search.github_ldap" do |payload| - result = - if @forest.empty? - @connection.search(options, &block) - else - @forest.each_with_object([]) do |(rootdn, server), res| - if options[:base].end_with?(rootdn) - rs = server.search(options, &block) - res.concat Array(rs) unless rs == false - end - end - end - return result - end - end - # Internal: Searches the host LDAP server's Root DSE for capabilities and # extensions. # @@ -224,8 +202,11 @@ def capabilities @capabilities ||= instrument "capabilities.github_ldap" do |payload| begin - rs = @connection.search(:ignore_server_caps => true, :base => "", :scope => Net::LDAP::SearchScope_BaseObject) - (rs and rs.first) + rs = @connection.search( + :ignore_server_caps => true, + :base => "", + :scope => Net::LDAP::SearchScope_BaseObject) + (rs and rs.first) || Net::LDAP::Entry.new rescue Net::LDAP::LdapError => error payload[:error] = error # stubbed result @@ -280,6 +261,16 @@ def configure_search_strategy(strategy = nil) configure_member_search_strategy(strategy) end + def configure_entry_search_strategy(use_forest_search) + @entry_search_strategy = begin + if use_forest_search && active_directory_capability? && capabilities[:configurationnamingcontext].any? + @entry_search_strategy = GitHub::Ldap::ForestSearch.new(@connection) + else + @entry_search_strategy = @connection + end + end + end + # Internal: Configure the membership validation strategy. # # If no known strategy is provided, detects ActiveDirectory capabilities or @@ -331,36 +322,6 @@ def configure_member_search_strategy(strategy = nil) end end - # Internal: Queries configuration for available domains - # - # Membership of local or global groups need to be evaluated by contacting referral Donmain Controllers - # - # Returns all Domain Controllers within the forest - def get_domain_forest(search_forest) - instrument "get_domain_forest.github_ldap" do |payload| - - # if we are talking to an active directory - if search_forest and active_directory_capability? and capabilities[:configurationnamingcontext].any? - domains = @connection.search( - base: capabilities[:configurationnamingcontext].first, - search_referrals: true, - filter: Net::LDAP::Filter.eq("nETBIOSName", "*") - ) - return domains.each_with_object({}) do |server, result| - if server[:ncname].any? and server[:dnsroot].any? - result[server[:ncname].first] = Net::LDAP.new({ - host: server[:dnsroot].first, - port: @connection.instance_variable_get(:@encryption)? 636 : 389, - auth: @connection.instance_variable_get(:@auth), - encryption: @connection.instance_variable_get(:@encryption), - instrumentation_service: @connection.instance_variable_get(:@instrumentation_service) - }) - end - end - end - return {} - end - end # Internal: Detect whether the LDAP host is an ActiveDirectory server. # diff --git a/lib/github/ldap/forest_search.rb b/lib/github/ldap/forest_search.rb new file mode 100644 index 0000000..8d15332 --- /dev/null +++ b/lib/github/ldap/forest_search.rb @@ -0,0 +1,57 @@ +module GitHub + class Ldap + class ForestSearch + + def initialize(connection) + @connection = connection + @forest = get_domain_forest + end + + def search(options, &block) + instrument "forest_search.github_ldap" do |payload| + result = + if @forest.empty? + @connection.search(options, &block) + else + @forest.each_with_object([]) do |(rootdn, server), res| + if options[:base].end_with?(rootdn) + rs = server.search(options, &block) + res.concat Array(rs) unless rs == false + end + end + end + return result + end + end + + private + + # Internal: Queries configuration for available domains + # + # Membership of local or global groups need to be evaluated by contacting referral Donmain Controllers + # + # Returns all Domain Controllers within the forest + def get_domain_forest + instrument "get_domain_forest.github_ldap" do |payload| + domains = @connection.search( + base: capabilities[:configurationnamingcontext].first, + search_referrals: true, + filter: Net::LDAP::Filter.eq("nETBIOSName", "*") + ) + return domains.each_with_object({}) do |server, result| + if server[:ncname].any? and server[:dnsroot].any? + result[server[:ncname].first] = Net::LDAP.new({ + host: server[:dnsroot].first, + port: @connection.instance_variable_get(:@encryption)? 636 : 389, + auth: @connection.instance_variable_get(:@auth), + encryption: @connection.instance_variable_get(:@encryption), + instrumentation_service: @connection.instance_variable_get(:@instrumentation_service) + }) + end + end + end + end + attr_reader :connection + end + end +end From 4a9e0823e14a92d1871248a10f444f4193fea18d Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Fri, 8 Jul 2016 13:38:42 -0500 Subject: [PATCH 04/26] Move connection attr_reader to a better spot --- lib/github/ldap/forest_search.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/github/ldap/forest_search.rb b/lib/github/ldap/forest_search.rb index 8d15332..cc6ffb9 100644 --- a/lib/github/ldap/forest_search.rb +++ b/lib/github/ldap/forest_search.rb @@ -26,6 +26,8 @@ def search(options, &block) private + attr_reader :connection + # Internal: Queries configuration for available domains # # Membership of local or global groups need to be evaluated by contacting referral Donmain Controllers @@ -51,7 +53,7 @@ def get_domain_forest end end end - attr_reader :connection + end end end From efcf91517b9d82b555dfa50d1133f35736030e17 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Fri, 8 Jul 2016 13:50:31 -0500 Subject: [PATCH 05/26] Docs & better structure for configure_entry_search_strategy --- lib/github/ldap.rb | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/github/ldap.rb b/lib/github/ldap.rb index c1639e7..cd64693 100644 --- a/lib/github/ldap.rb +++ b/lib/github/ldap.rb @@ -261,13 +261,18 @@ def configure_search_strategy(strategy = nil) configure_member_search_strategy(strategy) end + # Internal: Configure the entry search strategy. + # + # If the user has configured GHE to use forest searches AND we have an active + # Directory instance that has the right capabilities, use a ForestSearch + # strategy. Otherwise, the entry search strategy is simple the existing LDAP + # connection object. + # def configure_entry_search_strategy(use_forest_search) - @entry_search_strategy = begin - if use_forest_search && active_directory_capability? && capabilities[:configurationnamingcontext].any? - @entry_search_strategy = GitHub::Ldap::ForestSearch.new(@connection) - else - @entry_search_strategy = @connection - end + @entry_search_strategy = if use_forest_search && active_directory_capability? && capabilities[:configurationnamingcontext].any? + @entry_search_strategy = GitHub::Ldap::ForestSearch.new(@connection) + else + @entry_search_strategy = @connection end end @@ -322,7 +327,6 @@ def configure_member_search_strategy(strategy = nil) end end - # Internal: Detect whether the LDAP host is an ActiveDirectory server. # # See: http://msdn.microsoft.com/en-us/library/cc223359.aspx. From 50af684474771ddfda41e2aa44ae4cb5136ae066 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Fri, 8 Jul 2016 19:11:16 -0500 Subject: [PATCH 06/26] Require forest_search --- lib/github/ldap.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/github/ldap.rb b/lib/github/ldap.rb index cd64693..bd73bee 100644 --- a/lib/github/ldap.rb +++ b/lib/github/ldap.rb @@ -9,6 +9,7 @@ require 'github/ldap/virtual_attributes' require 'github/ldap/instrumentation' require 'github/ldap/member_search' +require 'github/ldap/forest_search' require 'github/ldap/membership_validators' module GitHub From c2cdb5286182b6d49bf96439eba24b7c47a78238 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Fri, 8 Jul 2016 19:11:59 -0500 Subject: [PATCH 07/26] Add naming context to ForestSearch --- lib/github/ldap/forest_search.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/github/ldap/forest_search.rb b/lib/github/ldap/forest_search.rb index cc6ffb9..730f21c 100644 --- a/lib/github/ldap/forest_search.rb +++ b/lib/github/ldap/forest_search.rb @@ -1,8 +1,14 @@ +require 'github/ldap/instrumentation' + module GitHub class Ldap + # For ActiveDirectory environments that have a forest with multiple domain controllers, + # this strategy class allows for entry searches across all domains in that forest. class ForestSearch + include Instrumentation - def initialize(connection) + def initialize(connection, naming_context) + @naming_context = naming_context @connection = connection @forest = get_domain_forest end @@ -26,7 +32,7 @@ def search(options, &block) private - attr_reader :connection + attr_reader :connection, :naming_context # Internal: Queries configuration for available domains # From b5a64c74574511dcbd7f7ff2bbd3b9e27b878b4d Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Fri, 8 Jul 2016 19:13:09 -0500 Subject: [PATCH 08/26] Better formatting --- lib/github/ldap/forest_search.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/github/ldap/forest_search.rb b/lib/github/ldap/forest_search.rb index 730f21c..1220984 100644 --- a/lib/github/ldap/forest_search.rb +++ b/lib/github/ldap/forest_search.rb @@ -20,10 +20,10 @@ def search(options, &block) @connection.search(options, &block) else @forest.each_with_object([]) do |(rootdn, server), res| - if options[:base].end_with?(rootdn) - rs = server.search(options, &block) - res.concat Array(rs) unless rs == false - end + if options[:base].end_with?(rootdn) + rs = server.search(options, &block) + res.concat Array(rs) unless rs == false + end end end return result From 95470202c0b8d7a5989f22251bff64c93ef40374 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Fri, 8 Jul 2016 19:13:31 -0500 Subject: [PATCH 09/26] Only search domains if not nil --- lib/github/ldap/forest_search.rb | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/github/ldap/forest_search.rb b/lib/github/ldap/forest_search.rb index 1220984..71a61fc 100644 --- a/lib/github/ldap/forest_search.rb +++ b/lib/github/ldap/forest_search.rb @@ -42,21 +42,24 @@ def search(options, &block) def get_domain_forest instrument "get_domain_forest.github_ldap" do |payload| domains = @connection.search( - base: capabilities[:configurationnamingcontext].first, + base: naming_context, search_referrals: true, filter: Net::LDAP::Filter.eq("nETBIOSName", "*") ) - return domains.each_with_object({}) do |server, result| - if server[:ncname].any? and server[:dnsroot].any? - result[server[:ncname].first] = Net::LDAP.new({ - host: server[:dnsroot].first, - port: @connection.instance_variable_get(:@encryption)? 636 : 389, - auth: @connection.instance_variable_get(:@auth), - encryption: @connection.instance_variable_get(:@encryption), - instrumentation_service: @connection.instance_variable_get(:@instrumentation_service) - }) + unless domains.nil? + return domains.each_with_object({}) do |server, result| + if server[:ncname].any? and server[:dnsroot].any? + result[server[:ncname].first] = Net::LDAP.new({ + host: server[:dnsroot].first, + port: @connection.instance_variable_get(:@encryption)? 636 : 389, + auth: @connection.instance_variable_get(:@auth), + encryption: @connection.instance_variable_get(:@encryption), + instrumentation_service: @connection.instance_variable_get(:@instrumentation_service) + }) + end end end + return {} end end From 186fa8b211f609ebb5268c12210f9579ad7d2874 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Fri, 8 Jul 2016 19:14:14 -0500 Subject: [PATCH 10/26] First stab at separate tests for ForestSearch --- test/forest_search_test.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 test/forest_search_test.rb diff --git a/test/forest_search_test.rb b/test/forest_search_test.rb new file mode 100644 index 0000000..67498a9 --- /dev/null +++ b/test/forest_search_test.rb @@ -0,0 +1,18 @@ +require_relative 'test_helper' + +class GitHubLdapForestSearchTest < GitHub::Ldap::Test + def setup + @connection = Net::LDAP.new({ + host: options[:host], + port: options[:port], + instrumentation_service: options[:instrumentation_service] + }) + #@connection.stub(:search, {}, ['search-forest']) + @forest_search = GitHub::Ldap::ForestSearch.new(@connection, "naming") + end + + def test_search + @forest_search.search({}) + assert true + end +end From 3abc142c4796dfb3695b3dad18e84105ff4abd99 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:32:16 -0500 Subject: [PATCH 11/26] Add mocha for service/LDAP mocking --- Gemfile | 1 + test/forest_search_test.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/Gemfile b/Gemfile index 4abbfe8..edf9461 100644 --- a/Gemfile +++ b/Gemfile @@ -5,4 +5,5 @@ gemspec group :test, :development do gem "byebug", :platforms => [:mri_20, :mri_21] + gem "mocha" end diff --git a/test/forest_search_test.rb b/test/forest_search_test.rb index 67498a9..081a830 100644 --- a/test/forest_search_test.rb +++ b/test/forest_search_test.rb @@ -1,4 +1,5 @@ require_relative 'test_helper' +require 'mocha/mini_test' class GitHubLdapForestSearchTest < GitHub::Ldap::Test def setup From 28d5019e9447fd448398de234acd15444e949693 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:34:50 -0500 Subject: [PATCH 12/26] Changed config name to 'use_forest_search' --- lib/github/ldap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/github/ldap.rb b/lib/github/ldap.rb index bd73bee..181a38d 100644 --- a/lib/github/ldap.rb +++ b/lib/github/ldap.rb @@ -85,7 +85,7 @@ def initialize(options = {}) end configure_virtual_attributes(options[:virtual_attributes]) - configure_entry_search_strategy(options[:search_forest]) + configure_entry_search_strategy(options[:use_forest_search]) # enable fallback recursive group search unless option is false @recursive_group_search_fallback = (options[:recursive_group_search_fallback] != false) From e3f791c102634ea73abc2971099438c6399175a0 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:35:54 -0500 Subject: [PATCH 13/26] Create LDAP object with ConfigurationNamingContext --- lib/github/ldap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/github/ldap.rb b/lib/github/ldap.rb index 181a38d..28a24ed 100644 --- a/lib/github/ldap.rb +++ b/lib/github/ldap.rb @@ -271,7 +271,7 @@ def configure_search_strategy(strategy = nil) # def configure_entry_search_strategy(use_forest_search) @entry_search_strategy = if use_forest_search && active_directory_capability? && capabilities[:configurationnamingcontext].any? - @entry_search_strategy = GitHub::Ldap::ForestSearch.new(@connection) + @entry_search_strategy = GitHub::Ldap::ForestSearch.new(@connection, capabilities[:configurationnamingcontext].first) else @entry_search_strategy = @connection end From 03c7d90c40d2e7df1b60609a28f9f5117c81595b Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:36:59 -0500 Subject: [PATCH 14/26] Added documentation; no need to create reference to search result --- lib/github/ldap/forest_search.rb | 41 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/github/ldap/forest_search.rb b/lib/github/ldap/forest_search.rb index 71a61fc..8aea6e4 100644 --- a/lib/github/ldap/forest_search.rb +++ b/lib/github/ldap/forest_search.rb @@ -7,26 +7,43 @@ class Ldap class ForestSearch include Instrumentation + # Build a new GitHub::Ldap::ForestSearch instance + # + # connection: GitHub::Ldap object representing the main AD connection. + # naming_context: The Distinguished Name (DN) of this forest's Configuration + # Naming Context, e.g., "CN=Configuration,DC=ad,DC=ghe,DC=com" + # + # See: https://technet.microsoft.com/en-us/library/aa998375(v=exchg.65).aspx + # def initialize(connection, naming_context) @naming_context = naming_context @connection = connection - @forest = get_domain_forest end + # Search over all domain controllers in the ActiveDirectory forest. + # + # options: options hash passed in from GitHub::Ldap#search + # &block: optional block passed in from GitHub::Ldap#search + # + # If no domain controllers are found in the forest, fall back on searching + # the main GitHub::Ldap object in @connection. + # + # If @forest is populated, iterate over each domain controller and perform + # the requested search, excluding domain controllers whose naming context + # is not in scope for the search base DN defined in options[:base]. + # def search(options, &block) - instrument "forest_search.github_ldap" do |payload| - result = - if @forest.empty? - @connection.search(options, &block) - else - @forest.each_with_object([]) do |(rootdn, server), res| - if options[:base].end_with?(rootdn) - rs = server.search(options, &block) - res.concat Array(rs) unless rs == false - end + instrument "forest_search.github_ldap" do + if forest.empty? + @connection.search(options, &block) + else + forest.each_with_object([]) do |(ncname, connection), res| + if options[:base].end_with?(ncname) + rs = connection.search(options, &block) + res.concat Array(rs) unless rs == false end end - return result + end end end From 8bdc1fabf419c7b4fe571b88286aadece837cf3d Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:37:56 -0500 Subject: [PATCH 15/26] Memoize forest object; added docs & general cleanup --- lib/github/ldap/forest_search.rb | 54 ++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/lib/github/ldap/forest_search.rb b/lib/github/ldap/forest_search.rb index 8aea6e4..605d536 100644 --- a/lib/github/ldap/forest_search.rb +++ b/lib/github/ldap/forest_search.rb @@ -53,30 +53,44 @@ def search(options, &block) # Internal: Queries configuration for available domains # - # Membership of local or global groups need to be evaluated by contacting referral Donmain Controllers + # Membership of local or global groups need to be evaluated by contacting referral + # Domain Controllers # - # Returns all Domain Controllers within the forest - def get_domain_forest - instrument "get_domain_forest.github_ldap" do |payload| - domains = @connection.search( - base: naming_context, - search_referrals: true, - filter: Net::LDAP::Filter.eq("nETBIOSName", "*") - ) - unless domains.nil? - return domains.each_with_object({}) do |server, result| - if server[:ncname].any? and server[:dnsroot].any? - result[server[:ncname].first] = Net::LDAP.new({ - host: server[:dnsroot].first, - port: @connection.instance_variable_get(:@encryption)? 636 : 389, - auth: @connection.instance_variable_get(:@auth), - encryption: @connection.instance_variable_get(:@encryption), - instrumentation_service: @connection.instance_variable_get(:@instrumentation_service) - }) + # returns: A memoized Hash of Domain Controllers from this AD forest in the format: + # + # { => } + # + # where "nCName" specifies the distinguished name of the naming context for the domain + # controller, and "connection" is an instance of Net::LDAP that represents a connection + # to that domain controller, for instance: + # + # {"DC=ad,DC=ghe,DC=local" => , + # "DC=fu,DC=bar,DC=local" => } + # + def forest + @forest ||= begin + instrument "get_domain_forest.github_ldap" do + domains = @connection.search( + base: naming_context, + search_referrals: true, + filter: Net::LDAP::Filter.eq("nETBIOSName", "*") + ) + if domains + domains.each_with_object({}) do |server, result| + if server[:ncname].any? && server[:dnsroot].any? + result[server[:ncname].first] = Net::LDAP.new({ + host: server[:dnsroot].first, + port: @connection.instance_variable_get(:@encryption)? 636 : 389, + auth: @connection.instance_variable_get(:@auth), + encryption: @connection.instance_variable_get(:@encryption), + instrumentation_service: @connection.instance_variable_get(:@instrumentation_service) + }) + end end + else + {} end end - return {} end end From 114367632e79823f59b60f7655cdd0b898067820 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:39:44 -0500 Subject: [PATCH 16/26] Init mock LDAP/connection with production-like naming context --- test/forest_search_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/forest_search_test.rb b/test/forest_search_test.rb index 081a830..9eb98d8 100644 --- a/test/forest_search_test.rb +++ b/test/forest_search_test.rb @@ -8,8 +8,8 @@ def setup port: options[:port], instrumentation_service: options[:instrumentation_service] }) - #@connection.stub(:search, {}, ['search-forest']) - @forest_search = GitHub::Ldap::ForestSearch.new(@connection, "naming") + configuration_naming_context = "CN=Configuration,DC=ad,DC=ghe,DC=local" + @forest_search = GitHub::Ldap::ForestSearch.new(@connection, configuration_naming_context) end def test_search From df990ab269193e31186723f319d0ca2d46942b17 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:40:51 -0500 Subject: [PATCH 17/26] Add test for falling back on @connection search when domains are nil --- test/forest_search_test.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/forest_search_test.rb b/test/forest_search_test.rb index 9eb98d8..100365d 100644 --- a/test/forest_search_test.rb +++ b/test/forest_search_test.rb @@ -12,7 +12,11 @@ def setup @forest_search = GitHub::Ldap::ForestSearch.new(@connection, configuration_naming_context) end - def test_search + def test_uses_connection_search_when_no_forest_present + # First search returns an empty Hash of domain controllers + @connection.expects(:search).returns({}) + # Since the forest is empty, should fall back on the base connection + @connection.expects(:search) @forest_search.search({}) assert true end From 1f40e828dfd4dbcfc8929b7610f18eb2b2c93e26 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:43:17 -0500 Subject: [PATCH 18/26] No need for assert --- test/forest_search_test.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/forest_search_test.rb b/test/forest_search_test.rb index 100365d..d9f5e3f 100644 --- a/test/forest_search_test.rb +++ b/test/forest_search_test.rb @@ -18,6 +18,5 @@ def test_uses_connection_search_when_no_forest_present # Since the forest is empty, should fall back on the base connection @connection.expects(:search) @forest_search.search({}) - assert true end end From a799cc4046496003d782f6c61fcbc8798aad2ba2 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:43:59 -0500 Subject: [PATCH 19/26] Test fall back on connection when domains are nil --- test/forest_search_test.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/forest_search_test.rb b/test/forest_search_test.rb index d9f5e3f..1639a1e 100644 --- a/test/forest_search_test.rb +++ b/test/forest_search_test.rb @@ -19,4 +19,13 @@ def test_uses_connection_search_when_no_forest_present @connection.expects(:search) @forest_search.search({}) end + + def test_uses_connection_search_when_domains_nil + # First search returns nil + @connection.expects(:search).returns(nil) + # Since the forest is empty, should fall back on the base connection + @connection.expects(:search) + @forest_search.search({}) + end + end From c06f0b081af088f5516d94adf53eea399ea20315 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:44:44 -0500 Subject: [PATCH 20/26] Test for iterating over domain controllers --- test/forest_search_test.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/forest_search_test.rb b/test/forest_search_test.rb index 1639a1e..057f36a 100644 --- a/test/forest_search_test.rb +++ b/test/forest_search_test.rb @@ -28,4 +28,29 @@ def test_uses_connection_search_when_domains_nil @forest_search.search({}) end + def test_iterates_over_domain_controllers_when_forest_present + mock_domains = Object.new + mock_domain_controller = Object.new + + # Mock out two Domain Controller connections (Net::LDAP objects) + mock_dc_connection1 = Object.new + mock_dc_connection2 = Object.new + rootdn = "DC=ad,DC=ghe,DC=local" + # Create a mock forest that contains the two mock DCs + # This assumes a single-l + forest = [[rootdn, mock_dc_connection1],[rootdn, mock_dc_connection2]] + + # First search returns the Hash of domain controllers + # This is what the forest is built from. + @connection.expects(:search).returns(mock_domains) + mock_domains.expects(:each_with_object).returns(forest) + + # Then we expect that a search will be performed on the LDAP object + # created from the returned forest of domain controllers + mock_dc_connection1.expects(:search) + mock_dc_connection2.expects(:search) + base = "CN=user1,CN=Users,DC=ad,DC=ghe,DC=local" + @forest_search.search({:base => base}) + end + end From 69f1e3ba519a6f3c73c13490cd76cbd922cec6d2 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:45:20 -0500 Subject: [PATCH 21/26] Test for concatenating search results --- test/forest_search_test.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/forest_search_test.rb b/test/forest_search_test.rb index 057f36a..a47b4b1 100644 --- a/test/forest_search_test.rb +++ b/test/forest_search_test.rb @@ -53,4 +53,23 @@ def test_iterates_over_domain_controllers_when_forest_present @forest_search.search({:base => base}) end + def test_returns_concatenated_search_results_from_forest + mock_domains = Object.new + mock_domain_controller = Object.new + + mock_dc_connection1 = Object.new + mock_dc_connection2 = Object.new + rootdn = "DC=ad,DC=ghe,DC=local" + forest = [[rootdn, mock_dc_connection1],[rootdn, mock_dc_connection2]] + + @connection.expects(:search).returns(mock_domains) + mock_domains.expects(:each_with_object).returns(forest) + + mock_dc_connection1.expects(:search).returns(["entry1"]) + mock_dc_connection2.expects(:search).returns(["entry2"]) + base = "CN=user1,CN=Users,DC=ad,DC=ghe,DC=local" + results = @forest_search.search({:base => base}) + assert_equal results, ["entry1", "entry2"] + end + end From 6078bdff183e2bb9928030c43863d77675c238df Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:45:53 -0500 Subject: [PATCH 22/26] Test rejecting searches from different rootdn --- test/forest_search_test.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/forest_search_test.rb b/test/forest_search_test.rb index a47b4b1..d46123d 100644 --- a/test/forest_search_test.rb +++ b/test/forest_search_test.rb @@ -72,4 +72,24 @@ def test_returns_concatenated_search_results_from_forest assert_equal results, ["entry1", "entry2"] end + def test_does_not_search_from_different_rootdn + mock_domains = Object.new + mock_domain_controller = Object.new + + mock_dc_connection1 = Object.new + mock_dc_connection2 = Object.new + forest = {"DC=ad,DC=ghe,DC=local" => mock_dc_connection1, + "DC=fu,DC=bar,DC=local" => mock_dc_connection2} + + @connection.expects(:search).returns(mock_domains) + mock_domains.expects(:each_with_object).returns(forest) + + mock_dc_connection1.expects(:search).returns(["entry1"]) + mock_dc_connection2.expects(:search).never + + base = "CN=user1,CN=Users,DC=ad,DC=ghe,DC=local" + results = @forest_search.search({:base => base}) + assert_equal results, ["entry1"] + end + end From 3b7974f03e33d88a8d324d96be559328b0e27532 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:46:22 -0500 Subject: [PATCH 23/26] Test searching domain controllers with differen subdomains --- test/forest_search_test.rb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/forest_search_test.rb b/test/forest_search_test.rb index d46123d..9778c21 100644 --- a/test/forest_search_test.rb +++ b/test/forest_search_test.rb @@ -92,4 +92,28 @@ def test_does_not_search_from_different_rootdn assert_equal results, ["entry1"] end + def test_searches_domain_controllers_from_differet_domains + server1 = {:ncname => ["DC=ghe,DC=local"], :dnsroot => ["ghe.local"]} + server2 = {:ncname => ["DC=ad,DC=ghe,DC=local"], :dnsroot => ["ad.ghe.local"]} + server3 = {:ncname => ["DC=eng,DC=ad,DC=ghe,DC=local"], :dnsroot => ["eng.ad.ghe.local"]} + mock_domains = [server1,server2,server3] + + mock_domain_controller = Object.new + @connection.expects(:search).returns(mock_domains) + + mock_dc_connection1 = Object.new + mock_dc_connection2 = Object.new + mock_dc_connection3 = Object.new + + Net::LDAP.expects(:new).returns(mock_dc_connection1) + Net::LDAP.expects(:new).returns(mock_dc_connection2) + Net::LDAP.expects(:new).returns(mock_dc_connection3) + + mock_dc_connection1.expects(:search).once + mock_dc_connection2.expects(:search).once + mock_dc_connection3.expects(:search).once + + base = "CN=user1,CN=Users,DC=eng,DC=ad,DC=ghe,DC=local" + @forest_search.search({:base => base}) + end end From afa1f6aa7a6f564607f3fdaac425480ef36bdede Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 09:52:12 -0500 Subject: [PATCH 24/26] Tweak to search documentation --- lib/github/ldap/forest_search.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/github/ldap/forest_search.rb b/lib/github/ldap/forest_search.rb index 605d536..359c6d5 100644 --- a/lib/github/ldap/forest_search.rb +++ b/lib/github/ldap/forest_search.rb @@ -22,8 +22,8 @@ def initialize(connection, naming_context) # Search over all domain controllers in the ActiveDirectory forest. # - # options: options hash passed in from GitHub::Ldap#search - # &block: optional block passed in from GitHub::Ldap#search + # options: is a hash with the same options that Net::LDAP::Connection#search supports. + # block: is an optional block to pass to the search. # # If no domain controllers are found in the forest, fall back on searching # the main GitHub::Ldap object in @connection. From 4e8f7b3b06c76c7f4d813b990588e23fc3f8b456 Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 12:04:15 -0500 Subject: [PATCH 25/26] Test name fix --- test/forest_search_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/forest_search_test.rb b/test/forest_search_test.rb index 9778c21..c372411 100644 --- a/test/forest_search_test.rb +++ b/test/forest_search_test.rb @@ -92,7 +92,7 @@ def test_does_not_search_from_different_rootdn assert_equal results, ["entry1"] end - def test_searches_domain_controllers_from_differet_domains + def test_searches_domain_controllers_from_different_domains server1 = {:ncname => ["DC=ghe,DC=local"], :dnsroot => ["ghe.local"]} server2 = {:ncname => ["DC=ad,DC=ghe,DC=local"], :dnsroot => ["ad.ghe.local"]} server3 = {:ncname => ["DC=eng,DC=ad,DC=ghe,DC=local"], :dnsroot => ["eng.ad.ghe.local"]} From 7cd27e8a33a2e20609fe001732358b5db8b9ebeb Mon Sep 17 00:00:00 2001 From: Dave Sims Date: Mon, 11 Jul 2016 20:57:53 -0500 Subject: [PATCH 26/26] minor doc cleanup --- test/forest_search_test.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/forest_search_test.rb b/test/forest_search_test.rb index c372411..d610a9a 100644 --- a/test/forest_search_test.rb +++ b/test/forest_search_test.rb @@ -37,7 +37,6 @@ def test_iterates_over_domain_controllers_when_forest_present mock_dc_connection2 = Object.new rootdn = "DC=ad,DC=ghe,DC=local" # Create a mock forest that contains the two mock DCs - # This assumes a single-l forest = [[rootdn, mock_dc_connection1],[rootdn, mock_dc_connection2]] # First search returns the Hash of domain controllers