diff --git a/.travis.yml b/.travis.yml index 09e4709..d9fd02b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,22 @@ 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 + +before_install: + - echo "deb http://ftp.br.debian.org/debian stable main" | sudo tee -a /etc/apt/sources.list + - sudo apt-get update + install: - if [ "$TESTENV" = "openldap" ]; then ./script/install-openldap; fi - bundle install diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ae6339..e082762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +# v1.10.1 + +* Bump net-ldap to 0.16.0 + # v1.10.0 * Bump net-ldap to 0.15.0 [#92](https://github.com/github/github-ldap/pull/92) 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/README.md b/README.md index 482f6ea..eb5fb01 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ There are a few configuration options required to use this adapter: * host: is the host address where the ldap server lives. * port: is the port where the ldap server lives. +* 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. * encryption: is the encryption protocol, disabled by default. The valid options are `ssl` and `tls`. * uid: is the field name in the ldap server used to authenticate your users, in ActiveDirectory this is `sAMAccountName`. diff --git a/github-ldap.gemspec b/github-ldap.gemspec index b8514d0..a2dad47 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.10.0" + spec.version = "1.10.1" 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.15.0' + spec.add_dependency 'net-ldap', '> 0.16.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 0545247..33e6627 100644 --- a/lib/github/ldap.rb +++ b/lib/github/ldap.rb @@ -10,6 +10,11 @@ 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 @@ class Ldap # # 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 # @@ -50,7 +61,13 @@ class Ldap # # host: required string ldap server host address # port: required string or number ldap server port + # hosts: an enumerable of pairs of hosts and corresponding ports with + # which to attempt opening connections (default [[host, port]]). Overrides + # host and port if set. # encryption: optional string. `ssl` or `tls`. nil by default + # tls_options: optional hash with TLS options for encrypted connections. + # Empty by default. See http://ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html + # for available values # admin_user: optional string ldap administrator user dn for authentication # admin_password: optional string ldap administrator user password # @@ -69,9 +86,15 @@ class Ldap def initialize(options = {}) @uid = options[:uid] || "sAMAccountName" + # Keep a reference to these as default auth for a Global Catalog if needed + @admin_user = options[:admin_user] + @admin_password = options[:admin_password] + @port = options[:port] + @connection = Net::LDAP.new({ host: options[:host], port: options[:port], + hosts: options[:hosts], instrumentation_service: options[:instrumentation_service] }) @@ -79,7 +102,7 @@ def initialize(options = {}) @connection.authenticate(options[:admin_user], options[:admin_password]) end - if encryption = check_encryption(options[:encryption]) + if encryption = check_encryption(options[:encryption], options[:tls_options]) @connection.encryption(encryption) end @@ -98,6 +121,9 @@ def initialize(options = {}) # 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 @@ -202,7 +228,7 @@ def capabilities instrument "capabilities.github_ldap" do |payload| begin @connection.search_root_dse - rescue Net::LDAP::LdapError => error + rescue Net::LDAP::Error => error payload[:error] = error # stubbed result Net::LDAP::Entry.new @@ -213,16 +239,18 @@ def capabilities # Internal - Determine whether to use encryption or not. # # encryption: is the encryption method, either 'ssl', 'tls', 'simple_tls' or 'start_tls'. + # tls_options: is the options hash for tls encryption method # # Returns the real encryption type. - def check_encryption(encryption) + def check_encryption(encryption, tls_options = {}) return unless encryption + tls_options ||= {} case encryption.downcase.to_sym when :ssl, :simple_tls - :simple_tls + { method: :simple_tls, tls_options: tls_options } when :tls, :start_tls - :start_tls + { method: :start_tls, tls_options: tls_options } end end @@ -281,6 +309,28 @@ def configure_membership_validation_strategy(strategy = nil) 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 @@ def valid_login?(login, password) # 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 @@ def perform(entry) # 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/script/install-openldap b/script/install-openldap index bb0033f..2deddad 100755 --- a/script/install-openldap +++ b/script/install-openldap @@ -13,10 +13,8 @@ TMPDIR=$(mktemp -d) cd $TMPDIR # Delete data and reconfigure. -sudo cp -v /var/lib/ldap/DB_CONFIG ./DB_CONFIG sudo rm -rf /etc/ldap/slapd.d/* sudo rm -rf /var/lib/ldap/* -sudo cp -v ./DB_CONFIG /var/lib/ldap/DB_CONFIG sudo slapadd -F /etc/ldap/slapd.d -b "cn=config" -l $BASE_PATH/slapd.conf.ldif # Load memberof and ref-int overlays and configure them. sudo slapadd -F /etc/ldap/slapd.d -b "cn=config" -l $BASE_PATH/memberof.ldif 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..4fc0dee 100644 --- a/test/domain_test.rb +++ b/test/domain_test.rb @@ -140,6 +140,15 @@ def test_auth_does_not_bind 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 @@ -227,7 +236,8 @@ def test_membership_for_posixGroups class GitHubLdapActiveDirectoryGroupsTest < GitHub::Ldap::Test def run(*) - self.class.test_env == "activedirectory" ? super : self + return super if self.class.test_env == "activedirectory" + Minitest::Result.from(self) end def test_filter_groups diff --git a/test/ldap_test.rb b/test/ldap_test.rb index c7b24c4..d5e9297 100644 --- a/test/ldap_test.rb +++ b/test/ldap_test.rb @@ -9,16 +9,44 @@ def test_connection_with_default_options assert @ldap.test_connection, "Ldap connection expected to succeed" end + def test_connection_with_list_of_hosts_with_one_valid_host + ldap = GitHub::Ldap.new(options.merge(hosts: [["localhost", options[:port]]])) + assert ldap.test_connection, "Ldap connection expected to succeed" + end + + def test_connection_with_list_of_hosts_with_first_valid + ldap = GitHub::Ldap.new(options.merge(hosts: [["localhost", options[:port]], ["invalid.local", options[:port]]])) + assert ldap.test_connection, "Ldap connection expected to succeed" + end + + def test_connection_with_list_of_hosts_with_first_invalid + ldap = GitHub::Ldap.new(options.merge(hosts: [["invalid.local", options[:port]], ["localhost", options[:port]]])) + assert ldap.test_connection, "Ldap connection expected to succeed" + end + def test_simple_tls - assert_equal :simple_tls, @ldap.check_encryption(:ssl) - assert_equal :simple_tls, @ldap.check_encryption('SSL') - assert_equal :simple_tls, @ldap.check_encryption(:simple_tls) + expected = { method: :simple_tls, tls_options: { } } + assert_equal expected, @ldap.check_encryption(:ssl) + assert_equal expected, @ldap.check_encryption('SSL') + assert_equal expected, @ldap.check_encryption(:simple_tls) end def test_start_tls - assert_equal :start_tls, @ldap.check_encryption(:tls) - assert_equal :start_tls, @ldap.check_encryption('TLS') - assert_equal :start_tls, @ldap.check_encryption(:start_tls) + expected = { method: :start_tls, tls_options: { } } + assert_equal expected, @ldap.check_encryption(:tls) + assert_equal expected, @ldap.check_encryption('TLS') + assert_equal expected, @ldap.check_encryption(:start_tls) + end + + def test_tls_validation + assert_equal({ method: :start_tls, tls_options: { verify_mode: OpenSSL::SSL::VERIFY_PEER } }, + @ldap.check_encryption(:tls, verify_mode: OpenSSL::SSL::VERIFY_PEER)) + assert_equal({ method: :start_tls, tls_options: { verify_mode: OpenSSL::SSL::VERIFY_NONE } }, + @ldap.check_encryption(:tls, verify_mode: OpenSSL::SSL::VERIFY_NONE)) + assert_equal({ method: :start_tls, tls_options: { cert_store: "some/path" } }, + @ldap.check_encryption(:tls, cert_store: "some/path")) + assert_equal({ method: :start_tls, tls_options: {} }, + @ldap.check_encryption(:tls, nil)) end def test_search_delegator @@ -114,6 +142,17 @@ def test_search_strategy_misconfigured_to_unrecognized_strategy_falls_back_to_de 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/member_search/active_directory_test.rb b/test/member_search/active_directory_test.rb index e3f367a..19f2c96 100644 --- a/test/member_search/active_directory_test.rb +++ b/test/member_search/active_directory_test.rb @@ -3,7 +3,8 @@ class GitHubLdapActiveDirectoryMemberSearchStubbedTest < GitHub::Ldap::Test # Only run when AD integration tests aren't run def run(*) - self.class.test_env != "activedirectory" ? super : self + return super if self.class.test_env != "activedirectory" + Minitest::Result.from(self) end def find_group(cn) @@ -46,7 +47,8 @@ def test_finds_deeply_nested_group_members class GitHubLdapActiveDirectoryMemberSearchIntegrationTest < GitHub::Ldap::Test # Only run this test suite if ActiveDirectory is configured def run(*) - self.class.test_env == "activedirectory" ? super : self + return super if self.class.test_env == "activedirectory" + Minitest::Result.from(self) end def find_group(cn) diff --git a/test/membership_validators/active_directory_test.rb b/test/membership_validators/active_directory_test.rb index 956fbc5..2160f8d 100644 --- a/test/membership_validators/active_directory_test.rb +++ b/test/membership_validators/active_directory_test.rb @@ -3,7 +3,8 @@ class GitHubLdapActiveDirectoryMembershipValidatorsStubbedTest < GitHub::Ldap::Test # Only run when AD integration tests aren't run def run(*) - self.class.test_env != "activedirectory" ? super : self + return super if self.class.test_env != "activedirectory" + Minitest::Result.from(self) end def setup @@ -72,7 +73,8 @@ def test_does_not_validate_user_not_in_any_group class GitHubLdapActiveDirectoryMembershipValidatorsIntegrationTest < GitHub::Ldap::Test # Only run this test suite if ActiveDirectory is configured def run(*) - self.class.test_env == "activedirectory" ? super : self + return super if self.class.test_env == "activedirectory" + Minitest::Result.from(self) end def setup 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..e92caa6 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -13,6 +13,8 @@ require 'minitest/mock' require 'minitest/autorun' +require 'mocha/minitest' + 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 @@ -29,8 +31,9 @@ def self.test_env def self.run(reporter, options = {}) start_server - super + result = super stop_server + result end def self.stop_server 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..32bed79 --- /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
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: