diff --git a/.travis.yml b/.travis.yml index 09e4709..798c5c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,18 @@ language: ruby rvm: - - 1.9.3 + - 2.0.0 - 2.1.0 env: - TESTENV=openldap - TESTENV=apacheds +# https://docs.travis-ci.com/user/hosts/ +addons: + hosts: + - ad1.ghe.dev + - ad2.ghe.dev + install: - if [ "$TESTENV" = "openldap" ]; then ./script/install-openldap; fi - bundle install diff --git a/CHANGELOG.md b/CHANGELOG.md index 70c0eed..2ae6339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +# v1.10.0 + +* Bump net-ldap to 0.15.0 [#92](https://github.com/github/github-ldap/pull/92) + # v1.9.0 * Update net-ldap dependency to `~> 0.11.0` [#84](https://github.com/github/github-ldap/pull/84) diff --git a/Gemfile b/Gemfile index 4abbfe8..a409814 100644 --- a/Gemfile +++ b/Gemfile @@ -6,3 +6,7 @@ gemspec group :test, :development do gem "byebug", :platforms => [:mri_20, :mri_21] end + +group :test do + gem "mocha" +end diff --git a/github-ldap.gemspec b/github-ldap.gemspec index b098983..b8514d0 100644 --- a/github-ldap.gemspec +++ b/github-ldap.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |spec| spec.name = "github-ldap" - spec.version = "1.9.1" + spec.version = "1.10.0" spec.authors = ["David Calavera", "Matt Todd"] spec.email = ["david.calavera@gmail.com", "chiology@gmail.com"] spec.description = %q{LDAP authentication for humans} @@ -15,7 +15,7 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] - spec.add_dependency 'net-ldap', '~> 0.14.0' + spec.add_dependency 'net-ldap', '~> 0.15.0' spec.add_development_dependency "bundler", "~> 1.3" spec.add_development_dependency 'ladle' diff --git a/lib/github/ldap.rb b/lib/github/ldap.rb index 90c6f92..b28e440 100644 --- a/lib/github/ldap.rb +++ b/lib/github/ldap.rb @@ -10,6 +10,11 @@ require 'github/ldap/virtual_attributes' require 'github/ldap/instrumentation' require 'github/ldap/member_search' require 'github/ldap/membership_validators' +require 'github/ldap/user_search/default' +require 'github/ldap/user_search/active_directory' +require 'github/ldap/connection_cache' +require 'github/ldap/referral_chaser' +require 'github/ldap/url' module GitHub class Ldap @@ -38,11 +43,17 @@ module GitHub # # Returns the return value of the block. def_delegator :@connection, :open + def_delegator :@connection, :host attr_reader :uid, :search_domains, :virtual_attributes, :membership_validator, :member_search_strategy, - :instrumentation_service + :instrumentation_service, + :user_search_strategy, + :connection, + :admin_user, + :admin_password, + :port # Build a new GitHub::Ldap instance # @@ -72,6 +83,11 @@ module GitHub def initialize(options = {}) @uid = options[:uid] || "sAMAccountName" + # Keep a reference to these as default auth for a Global Catalog if needed + @admin_user = options[:admin_user] + @admin_password = options[:admin_password] + @port = options[:port] + @connection = Net::LDAP.new({ host: options[:host], port: options[:port], @@ -102,6 +118,9 @@ module GitHub # configure both the membership validator and the member search strategies configure_search_strategy(options[:search_strategy]) + # configure the strategy used by Domain#user? to look up a user entry for login + configure_user_search_strategy(options[:user_search_strategy]) + # enables instrumenting queries @instrumentation_service = options[:instrumentation_service] end @@ -285,6 +304,28 @@ module GitHub end end + # Internal: Set the user search strategy that will be used by + # Domain#user?. + # + # strategy - Can be either 'default' or 'global_catalog'. + # 'default' strategy will search the configured + # domain controller with a search base relative + # to the controller's domain context. + # 'global_catalog' will search the entire forest + # using Active Directory's Global Catalog + # functionality. + def configure_user_search_strategy(strategy) + @user_search_strategy = + case strategy.to_s + when "default" + GitHub::Ldap::UserSearch::Default.new(self) + when "global_catalog" + GitHub::Ldap::UserSearch::ActiveDirectory.new(self) + else + GitHub::Ldap::UserSearch::Default.new(self) + end + end + # Internal: Configure the member search strategy. # # diff --git a/lib/github/ldap/connection_cache.rb b/lib/github/ldap/connection_cache.rb new file mode 100644 index 0000000..d2feab9 --- /dev/null +++ b/lib/github/ldap/connection_cache.rb @@ -0,0 +1,26 @@ +module GitHub + class Ldap + + # A simple cache of GitHub::Ldap objects to prevent creating multiple + # instances of connections that point to the same URI/host. + class ConnectionCache + + # Public - Create or return cached instance of GitHub::Ldap created with options, + # where the cache key is the value of options[:host]. + # + # options - Initialization attributes suitable for creating a new connection with + # GitHub::Ldap.new(options) + # + # Returns true or false. + def self.get_connection(options={}) + @cache ||= self.new + @cache.get_connection(options) + end + + def get_connection(options) + @connections ||= {} + @connections[options[:host]] ||= GitHub::Ldap.new(options) + end + end + end +end diff --git a/lib/github/ldap/domain.rb b/lib/github/ldap/domain.rb index 8fd904f..07af950 100644 --- a/lib/github/ldap/domain.rb +++ b/lib/github/ldap/domain.rb @@ -115,10 +115,7 @@ module GitHub # Returns the user if the login matches any `uid`. # Returns nil if there are no matches. def user?(login, search_options = {}) - options = search_options.merge \ - filter: login_filter(@uid, login), - size: 1 - search(options).first + @ldap.user_search_strategy.perform(login, @base_name, @uid, search_options).first end # Check if a user can be bound with a password. diff --git a/lib/github/ldap/membership_validators/active_directory.rb b/lib/github/ldap/membership_validators/active_directory.rb index 69b9bc8..ff4e4fc 100644 --- a/lib/github/ldap/membership_validators/active_directory.rb +++ b/lib/github/ldap/membership_validators/active_directory.rb @@ -24,15 +24,23 @@ module GitHub # Sets the entry to the base and scopes the search to the base, # according to the source documentation, found here: # http://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx - matched = ldap.search \ + # + # Use ReferralChaser to chase any potential referrals for an entry that may be owned by a different + # domain controller. + matched = referral_chaser.search \ filter: membership_in_chain_filter(entry), base: entry.dn, scope: Net::LDAP::SearchScope_BaseObject, + return_referrals: true, attributes: ATTRS # membership validated if entry was matched and returned as a result # Active Directory DNs are case-insensitive - matched.map { |m| m.dn.downcase }.include?(entry.dn.downcase) + Array(matched).map { |m| m.dn.downcase }.include?(entry.dn.downcase) + end + + def referral_chaser + @referral_chaser ||= GitHub::Ldap::ReferralChaser.new(@ldap) end # Internal: Constructs a membership filter using the "in chain" diff --git a/lib/github/ldap/referral_chaser.rb b/lib/github/ldap/referral_chaser.rb new file mode 100644 index 0000000..4811c51 --- /dev/null +++ b/lib/github/ldap/referral_chaser.rb @@ -0,0 +1,98 @@ +module GitHub + class Ldap + + # This class adds referral chasing capability to a GitHub::Ldap connection. + # + # See: https://technet.microsoft.com/en-us/library/cc978014.aspx + # http://www.umich.edu/~dirsvcs/ldap/doc/other/ldap-ref.html + # + class ReferralChaser + + # Public - Creates a ReferralChaser that decorates an instance of GitHub::Ldap + # with additional functionality to the #search method, allowing it to chase + # any referral entries and aggregate the results into a single response. + # + # connection - The instance of GitHub::Ldap to use for searching. Will use + # the connection's authentication, (admin_user and admin_password) as credentials + # for connecting to referred domain controllers. + def initialize(connection) + @connection = connection + @admin_user = connection.admin_user + @admin_password = connection.admin_password + @port = connection.port + end + + # Public - Search the domain controller represented by this instance's connection. + # If a referral is returned, search only one of the domain controllers indicated + # by the referral entries, per RFC 4511 (https://tools.ietf.org/html/rfc4511): + # + # "If the client wishes to progress the operation, it contacts one of + # the supported services found in the referral. If multiple URIs are + # present, the client assumes that any supported URI may be used to + # progress the operation." + # + # options - is a hash with the same options that Net::LDAP::Connection#search supports. + # Referral searches will use the given options, but will replace options[:base] + # with the referral URL's base search dn. + # + # Does not take a block argument as GitHub::Ldap and Net::LDAP::Connection#search do. + # + # Will not recursively follow any subsequent referrals. + # + # Returns an Array of Net::LDAP::Entry. + def search(options) + search_results = [] + referral_entries = [] + + search_results = connection.search(options) do |entry| + if entry && entry[:search_referrals] + referral_entries << entry + end + end + + unless referral_entries.empty? + entry = referral_entries.first + referral_string = entry[:search_referrals].first + if GitHub::Ldap::URL.valid?(referral_string) + referral = Referral.new(referral_string, admin_user, admin_password, port) + search_results = referral.search(options) + end + end + + Array(search_results) + end + + private + + attr_reader :connection, :admin_user, :admin_password, :port + + # Represents a referral entry from an LDAP search result. Constructs a corresponding + # GitHub::Ldap object from the paramaters on the referral_url and provides a #search + # method to continue the search on the referred domain. + class Referral + def initialize(referral_url, admin_user, admin_password, port=nil) + url = GitHub::Ldap::URL.new(referral_url) + @search_base = url.dn + + connection_options = { + host: url.host, + port: port || url.port, + scope: url.scope, + admin_user: admin_user, + admin_password: admin_password + } + + @connection = GitHub::Ldap::ConnectionCache.get_connection(connection_options) + end + + # Search the referred domain controller with options, merging in the referred search + # base DN onto options[:base]. + def search(options) + connection.search(options.merge(base: search_base)) + end + + attr_reader :search_base, :connection + end + end + end +end diff --git a/lib/github/ldap/url.rb b/lib/github/ldap/url.rb new file mode 100644 index 0000000..5c733a7 --- /dev/null +++ b/lib/github/ldap/url.rb @@ -0,0 +1,87 @@ +module GitHub + class Ldap + + # This class represents an LDAP URL + # + # See: https://tools.ietf.org/html/rfc4516#section-2 + # https://docs.oracle.com/cd/E19957-01/817-6707/urls.html + # + class URL + extend Forwardable + SCOPES = { + "base" => Net::LDAP::SearchScope_BaseObject, + "one" => Net::LDAP::SearchScope_SingleLevel, + "sub" => Net::LDAP::SearchScope_WholeSubtree + } + SCOPES.default = Net::LDAP::SearchScope_BaseObject + + attr_reader :dn, :attributes, :scope, :filter + + def_delegators :@uri, :port, :host, :scheme + + # Public - Creates a new GitHub::Ldap::URL object with :port, :host and :scheme + # delegated to a URI object parsed from url_string, and then parses the + # query params according to the LDAP specification. + # + # url_string - An LDAP URL string. + # returns - a GitHub::Ldap::URL with the following attributes: + # host - Name or IP of the LDAP server. + # port - The given port, defaults to 389. + # dn - The base search DN. + # attributes - The comma-delimited list of attributes to be returned. + # scope - The scope of the search. + # filter - Search filter to apply to entries within the specified scope of the search. + # + # Supported LDAP URL strings look like this, where sections in brackets are optional: + # + # ldap[s]://[hostport][/[dn[?[attributes][?[scope][?[filter]]]]]] + # + # where: + # + # hostport is a host name with an optional ":portnumber" + # dn is the base DN to be used for an LDAP search operation + # attributes is a comma separated list of attributes to be retrieved + # scope is one of these three strings: base one sub (default=base) + # filter is LDAP search filter as used in a call to ldap_search + # + # For example: + # + # ldap://dc4.ghe.local:456/CN=Maggie,DC=dc4,DC=ghe,DC=local?cn,mail?base?(cn=Charlie) + # + def initialize(url_string) + if !self.class.valid?(url_string) + raise InvalidLdapURLException.new("Invalid LDAP URL: #{url_string}") + end + @uri = URI(url_string) + @dn = URI.unescape(@uri.path.sub(/^\//, "")) + if @uri.query + @attributes, @scope, @filter = @uri.query.split("?") + end + end + + def self.valid?(url_string) + url_string =~ URI::regexp && ["ldap", "ldaps"].include?(URI(url_string).scheme) + end + + # Maps the returned scope value from the URL to one of Net::LDAP::Scopes + # + # The URL scope value can be one of: + # "base" - retrieves information only about the DN (base_dn) specified. + # "one" - retrieves information about entries one level below the DN (base_dn) specified. The base entry is not included in this scope. + # "sub" - retrieves information about entries at all levels below the DN (base_dn) specified. The base entry is included in this scope. + # + # Which will map to one of the following Net::LDAP::Scopes: + # SearchScope_BaseObject = 0 + # SearchScope_SingleLevel = 1 + # SearchScope_WholeSubtree = 2 + # + # If no scope or an invalid scope is given, defaults to SearchScope_BaseObject + def net_ldap_scope + Net::LDAP::SearchScopes[SCOPES[scope]] + end + + class InvalidLdapURLException < Exception; end + end + end +end + diff --git a/lib/github/ldap/user_search/active_directory.rb b/lib/github/ldap/user_search/active_directory.rb new file mode 100644 index 0000000..2bec4ad --- /dev/null +++ b/lib/github/ldap/user_search/active_directory.rb @@ -0,0 +1,51 @@ +module GitHub + class Ldap + module UserSearch + class ActiveDirectory < Default + + private + + # Private - Overridden from base class to set the base to "", and use the + # Global Catalog to perform the user search. + def search(search_options) + Array(global_catalog_connection.search(search_options.merge(options))) + end + + def global_catalog_connection + GlobalCatalog.connection(ldap) + end + + # When doing a global search for a user's DN, set the search base to blank + def options + super.merge(base: "") + end + end + + class GlobalCatalog < Net::LDAP + STANDARD_GC_PORT = 3268 + LDAPS_GC_PORT = 3269 + + # Returns a connection to the Active Directory Global Catalog + # + # See: https://technet.microsoft.com/en-us/library/cc728188(v=ws.10).aspx + # + def self.connection(ldap) + @global_catalog_instance ||= begin + netldap = ldap.connection + # This is ugly, but Net::LDAP doesn't expose encryption or auth + encryption = netldap.instance_variable_get(:@encryption) + auth = netldap.instance_variable_get(:@auth) + + new({ + host: ldap.host, + instrumentation_service: ldap.instrumentation_service, + port: encryption ? LDAPS_GC_PORT : STANDARD_GC_PORT, + auth: auth, + encryption: encryption + }) + end + end + end + end + end +end diff --git a/lib/github/ldap/user_search/default.rb b/lib/github/ldap/user_search/default.rb new file mode 100644 index 0000000..2f1aa3f --- /dev/null +++ b/lib/github/ldap/user_search/default.rb @@ -0,0 +1,40 @@ +module GitHub + class Ldap + module UserSearch + # The default user search strategy, mainly for allowing Domain#user? to + # search for a user on the configured domain controller, or use the Global + # Catalog to search across the entire Active Directory forest. + class Default + include Filter + + def initialize(ldap) + @ldap = ldap + @options = { + :attributes => [], + :paged_searches_supported => true, + :size => 1 + } + end + + # Performs a normal search on the configured domain controller + # using the default base DN, uid, search_options + def perform(login, base_name, uid, search_options) + search_options[:filter] = login_filter(uid, login) + search_options[:base] = base_name + search(options.merge(search_options)) + end + + # The default search. This can be overridden by a child class + # like GitHub::Ldap::UserSearch::ActiveDirectory to change the + # scope of the search. + def search(options) + ldap.search(options) + end + + private + + attr_reader :options, :ldap + end + end + end +end diff --git a/test/connection_cache_test.rb b/test/connection_cache_test.rb new file mode 100644 index 0000000..1b55a6b --- /dev/null +++ b/test/connection_cache_test.rb @@ -0,0 +1,18 @@ +require_relative 'test_helper' + +class GitHubLdapConnectionCacheTestCases < GitHub::Ldap::Test + + def test_returns_cached_connection + conn1 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad1.ghe.dev")) + conn2 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad1.ghe.dev")) + assert_equal conn1.object_id, conn2.object_id + end + + def test_creates_new_connections_per_host + conn1 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad1.ghe.dev")) + conn2 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad2.ghe.dev")) + conn3 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad2.ghe.dev")) + refute_equal conn1.object_id, conn2.object_id + assert_equal conn2.object_id, conn3.object_id + end +end diff --git a/test/domain_test.rb b/test/domain_test.rb index 4480e60..d54850c 100644 --- a/test/domain_test.rb +++ b/test/domain_test.rb @@ -140,6 +140,15 @@ module GitHubLdapDomainTestCases assert user = @domain.user?('user1') refute @domain.auth(user, 'foo'), 'Expected user not not bind' end + + def test_user_search_returns_first_entry + entry = mock("Net::Ldap::Entry") + search_strategy = mock("GitHub::Ldap::UserSearch::Default") + search_strategy.stubs(:perform).returns([entry]) + @ldap.expects(:user_search_strategy).returns(search_strategy) + user = @domain.user?('user1', :attributes => [:cn]) + assert_equal entry, user + end end class GitHubLdapDomainTest < GitHub::Ldap::Test diff --git a/test/ldap_test.rb b/test/ldap_test.rb index d00c7a0..8105fb8 100644 --- a/test/ldap_test.rb +++ b/test/ldap_test.rb @@ -129,6 +129,17 @@ module GitHubLdapTestCases assert_equal GitHub::Ldap::MemberSearch::Recursive, @ldap.member_search_strategy end + def test_user_search_strategy_global_catalog_when_configured + @ldap.configure_user_search_strategy("global_catalog") + assert_kind_of GitHub::Ldap::UserSearch::ActiveDirectory, @ldap.user_search_strategy + end + + def test_user_search_strategy_default_when_configured + @ldap.configure_user_search_strategy("default") + refute_kind_of GitHub::Ldap::UserSearch::ActiveDirectory, @ldap.user_search_strategy + assert_kind_of GitHub::Ldap::UserSearch::Default, @ldap.user_search_strategy + end + def test_capabilities assert_kind_of Net::LDAP::Entry, @ldap.capabilities end diff --git a/test/referral_chaser_test.rb b/test/referral_chaser_test.rb new file mode 100644 index 0000000..3a19973 --- /dev/null +++ b/test/referral_chaser_test.rb @@ -0,0 +1,102 @@ +require_relative 'test_helper' + +class GitHubLdapReferralChaserTestCases < GitHub::Ldap::Test + + def setup + @ldap = GitHub::Ldap.new(options) + @chaser = GitHub::Ldap::ReferralChaser.new(@ldap) + end + + def test_creates_referral_with_connection_credentials + @ldap.expects(:search).yields({ search_referrals: ["ldap://dc1.ghe.local/"]}).returns([]) + + referral = mock("GitHub::Ldap::ReferralChaser::Referral") + referral.stubs(:search).returns([]) + + GitHub::Ldap::ReferralChaser::Referral.expects(:new) + .with("ldap://dc1.ghe.local/", "uid=admin,dc=github,dc=com", "passworD1", options[:port]) + .returns(referral) + + @chaser.search({}) + end + + def test_creates_referral_with_default_port + @ldap.expects(:search).yields({ + search_referrals: ["ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"] + }).returns([]) + + stub_referral_connection = mock("GitHub::Ldap") + stub_referral_connection.stubs(:search).returns([]) + GitHub::Ldap::ConnectionCache.expects(:get_connection).with(has_entry(port: options[:port])).returns(stub_referral_connection) + chaser = GitHub::Ldap::ReferralChaser.new(@ldap) + chaser.search({}) + end + + def test_creates_referral_for_first_referral_string + @ldap.expects(:search).multiple_yields([ + { search_referrals: + ["ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", + "ldap://dc2.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"] + } + ],[ + { search_referrals: + ["ldap://dc3.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", + "ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"] + } + ]).returns([]) + + referral = mock("GitHub::Ldap::ReferralChaser::Referral") + referral.stubs(:search).returns([]) + + GitHub::Ldap::ReferralChaser::Referral.expects(:new) + .with( + "ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", + "uid=admin,dc=github,dc=com", + "passworD1", + options[:port]) + .returns(referral) + + @chaser.search({}) + end + + def test_returns_referral_search_results + @ldap.expects(:search).multiple_yields([ + { search_referrals: + ["ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", + "ldap://dc2.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"] + } + ],[ + { search_referrals: + ["ldap://dc3.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", + "ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"] + } + ]).returns([]) + + referral = mock("GitHub::Ldap::ReferralChaser::Referral") + referral.expects(:search).returns(["result", "result"]) + + GitHub::Ldap::ReferralChaser::Referral.expects(:new).returns(referral) + + results = @chaser.search({}) + assert_equal(["result", "result"], results) + end + + def test_handle_blank_url_string_in_referral + @ldap.expects(:search).yields({ search_referrals: [""] }) + + results = @chaser.search({}) + assert_equal([], results) + end + + def test_returns_referral_search_results + @ldap.expects(:search).yields({ foo: ["not a referral"] }) + + GitHub::Ldap::ReferralChaser::Referral.expects(:new).never + results = @chaser.search({}) + end + + def test_referral_should_use_host_from_referral_string + GitHub::Ldap::ConnectionCache.expects(:get_connection).with(has_entry(host: "dc4.ghe.local")) + GitHub::Ldap::ReferralChaser::Referral.new("ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", "", "") + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 5beca09..856cb5c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -13,6 +13,8 @@ require 'github/ldap/server' require 'minitest/mock' require 'minitest/autorun' +require 'mocha/mini_test' + if ENV.fetch('TESTENV', "apacheds") == "apacheds" # Make sure we clean up running test server # NOTE: We need to do this manually since its internal `at_exit` hook diff --git a/test/url_test.rb b/test/url_test.rb new file mode 100644 index 0000000..db44ce2 --- /dev/null +++ b/test/url_test.rb @@ -0,0 +1,85 @@ +require_relative 'test_helper' + +class GitHubLdapURLTestCases < GitHub::Ldap::Test + + def setup + @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)") + end + + def test_host + assert_equal "dc4.ghe.local", @url.host + end + + def test_port + assert_equal 123, @url.port + end + + def test_scheme + assert_equal "ldap", @url.scheme + end + + def test_default_port + url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local?attributes?scope?filter") + assert_equal 389, url.port + end + + def test_simple_url + url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local") + assert_equal 389, url.port + assert_equal "dc4.ghe.local", url.host + assert_equal "ldap", url.scheme + assert_equal "", url.dn + assert_equal nil, url.attributes + assert_equal nil, url.filter + assert_equal nil, url.scope + end + + def test_invalid_scheme + ex = assert_raises(GitHub::Ldap::URL::InvalidLdapURLException) do + GitHub::Ldap::URL.new("http://dc4.ghe.local") + end + assert_equal("Invalid LDAP URL: http://dc4.ghe.local", ex.message) + end + + def test_invalid_url + ex = assert_raises(GitHub::Ldap::URL::InvalidLdapURLException) do + GitHub::Ldap::URL.new("not a url") + end + assert_equal("Invalid LDAP URL: not a url", ex.message) + end + + def test_parse_dn + assert_equal "CN=Maggie Mae,CN=Users,DC=dc4,DC=ghe,DC=local", @url.dn + end + + def test_parse_attributes + assert_equal "cn,mail,telephoneNumber", @url.attributes + end + + def test_parse_filter + assert_equal "(cn=Charlie)", @url.filter + end + + def test_parse_scope + assert_equal "base", @url.scope + end + + def test_default_scope + url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/base_dn?cn=joe??filter") + assert_equal "", url.scope + end + + def test_net_ldap_scopes + sub_scope_url = GitHub::Ldap::URL.new("ldap://ghe.local/base_dn?cn=joe?sub?filter") + one_scope_url = GitHub::Ldap::URL.new("ldap://ghe.local/base_dn?cn=joe?one?filter") + base_scope_url = GitHub::Ldap::URL.new("ldap://ghe.local/base_dn?cn=joe?base?filter") + default_scope_url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/base_dn?cn=joe??filter") + invalid_scope_url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/base_dn?cn=joe?invalid?filter") + + assert_equal Net::LDAP::SearchScope_BaseObject, base_scope_url.net_ldap_scope + assert_equal Net::LDAP::SearchScope_SingleLevel, one_scope_url.net_ldap_scope + assert_equal Net::LDAP::SearchScope_WholeSubtree, sub_scope_url.net_ldap_scope + assert_equal Net::LDAP::SearchScope_BaseObject, default_scope_url.net_ldap_scope + assert_equal Net::LDAP::SearchScope_BaseObject, invalid_scope_url.net_ldap_scope + end +end diff --git a/test/user_search/active_directory_test.rb b/test/user_search/active_directory_test.rb new file mode 100644 index 0000000..2aec199 --- /dev/null +++ b/test/user_search/active_directory_test.rb @@ -0,0 +1,53 @@ +require_relative '../test_helper' + +class GitHubLdapActiveDirectoryUserSearchTests < GitHub::Ldap::Test + + def test_global_catalog_returns_empty_array_for_no_results + ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev')) + ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(ldap) + + mock_global_catalog_connection = mock("GitHub::Ldap::UserSearch::GlobalCatalog") + mock_global_catalog_connection.expects(:search).returns(nil) + ad_user_search.expects(:global_catalog_connection).returns(mock_global_catalog_connection) + results = ad_user_search.perform("login", "CN=Joe", "uid", {}) + assert_equal [], results + end + + def test_global_catalog_returns_array_of_results + ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev')) + ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(ldap) + + mock_global_catalog_connection = mock("GitHub::Ldap::UserSearch::GlobalCatalog") + stub_entry = mock("Net::LDAP::Entry") + + mock_global_catalog_connection.expects(:search).returns(stub_entry) + ad_user_search.expects(:global_catalog_connection).returns(mock_global_catalog_connection) + + results = ad_user_search.perform("login", "CN=Joe", "uid", {}) + assert_equal [stub_entry], results + end + + def test_searches_with_empty_base_dn + ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev')) + ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(ldap) + + mock_global_catalog_connection = mock("GitHub::Ldap::UserSearch::GlobalCatalog") + mock_global_catalog_connection.expects(:search).with(has_entry(:base => "")) + ad_user_search.expects(:global_catalog_connection).returns(mock_global_catalog_connection) + ad_user_search.perform("login", "CN=Joe", "uid", {}) + end + + def test_global_catalog_default_settings + ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev')) + global_catalog = GitHub::Ldap::UserSearch::GlobalCatalog.connection(ldap) + instrumentation_service = global_catalog.instance_variable_get(:@instrumentation_service) + + auth = global_catalog.instance_variable_get(:@auth) + assert_equal :simple, auth[:method] + assert_equal "uid=admin,dc=github,dc=com", auth[:username] + assert_equal "passworD1", auth[:password] + assert_equal "ghe.dev", global_catalog.host + assert_equal 3268, global_catalog.port + assert_equal "MockInstrumentationService", instrumentation_service.class.name + end +end diff --git a/test/user_search/default_test.rb b/test/user_search/default_test.rb new file mode 100644 index 0000000..abc230a --- /dev/null +++ b/test/user_search/default_test.rb @@ -0,0 +1,19 @@ +require_relative '../test_helper' + +class GitHubLdapActiveDirectoryUserSearchTests < GitHub::Ldap::Test + def setup + @ldap = GitHub::Ldap.new(options) + @default_user_search = GitHub::Ldap::UserSearch::Default.new(@ldap) + end + + def test_default_search_options + @ldap.expects(:search).with(has_entries( + attributes: [], + size: 1, + paged_searches_supported: true, + base: "CN=HI,CN=McDunnough", + filter: kind_of(Net::LDAP::Filter) + )) + @default_user_search.perform("","CN=HI,CN=McDunnough","",{}) + end +end