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 38f8020..e082762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # 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) + +# v1.9.0 + +* Update net-ldap dependency to `~> 0.11.0` [#84](https://github.com/github/github-ldap/pull/84) + +# v1.8.2 + +* Ignore case when comparing ActiveDirectory DNs [#82](https://github.com/github/github-ldap/pull/82) + +# v1.8.1 + +* Expand supported ActiveDirectory capabilities to include Windows Server 2003 [#80](https://github.com/github/github-ldap/pull/80) + +# v1.8.0 + +* Optimize Recursive *Member Search* strategy [#78](https://github.com/github/github-ldap/pull/78) + +# v1.7.1 + +* Add Active Directory group filter [#75](https://github.com/github/github-ldap/pull/75) + +## v1.7.0 + +* Accept `:depth` option for Recursive membership validator strategy instance [#73](https://github.com/github/github-ldap/pull/73) +* Deprecate `depth` argument to `Recursive` membership validator `perform` method +* Bump net-ldap dependency to 0.10.0 at minimum [#72](https://github.com/github/github-ldap/pull/72) + ## v1.6.0 * Expose `GitHub::Ldap::Group.group?` for testing if entry is a group [#67](https://github.com/github/github-ldap/pull/67) 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 5b526de..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.6.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.9.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 ebdfddc..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 @@ -19,7 +24,7 @@ class Ldap # Internal: The capability required to use ActiveDirectory features. # See: http://msdn.microsoft.com/en-us/library/cc223359.aspx. - ACTIVE_DIRECTORY_V61_R2_OID = "1.2.840.113556.1.4.2080".freeze + ACTIVE_DIRECTORY_V51_OID = "1.2.840.113556.1.4.1670".freeze # Utility method to get the last operation result with a human friendly message. # @@ -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. # # @@ -313,7 +363,7 @@ def configure_member_search_strategy(strategy = nil) # # Returns true if the host is an ActiveDirectory server, false otherwise. def active_directory_capability? - capabilities[:supportedcapabilities].include?(ACTIVE_DIRECTORY_V61_R2_OID) + capabilities[:supportedcapabilities].include?(ACTIVE_DIRECTORY_V51_OID) end private :active_directory_capability? end 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/filter.rb b/lib/github/ldap/filter.rb index 64f5aa3..6f62af3 100644 --- a/lib/github/ldap/filter.rb +++ b/lib/github/ldap/filter.rb @@ -3,7 +3,8 @@ class Ldap module Filter ALL_GROUPS_FILTER = Net::LDAP::Filter.eq("objectClass", "groupOfNames") | Net::LDAP::Filter.eq("objectClass", "groupOfUniqueNames") | - Net::LDAP::Filter.eq("objectClass", "posixGroup") + Net::LDAP::Filter.eq("objectClass", "posixGroup") | + Net::LDAP::Filter.eq("objectClass", "group") MEMBERSHIP_NAMES = %w(member uniqueMember) diff --git a/lib/github/ldap/member_search/recursive.rb b/lib/github/ldap/member_search/recursive.rb index 5fe2489..a36aa4d 100644 --- a/lib/github/ldap/member_search/recursive.rb +++ b/lib/github/ldap/member_search/recursive.rb @@ -34,73 +34,98 @@ def initialize(ldap, options = {}) # # Returns Array of Net::LDAP::Entry objects. def perform(group) + # track groups found found = Hash.new - # find members (N queries) - entries = member_entries(group) - return [] if entries.empty? + # track all DNs searched for (so we don't repeat searches) + searched = Set.new - # track found entries - entries.each do |entry| - found[entry.dn] = entry + # if this is a posixGroup, return members immediately (no nesting) + uids = member_uids(group) + return entries_by_uid(uids) if uids.any? + + # track group + searched << group.dn + found[group.dn] = group + + # pull out base group's member DNs + dns = member_dns(group) + + # search for base group's subgroups + groups = dns.each_with_object([]) do |dn, groups| + groups.concat find_groups_by_dn(dn) + searched << dn end - # descend to `depth` levels, at most - depth.times do |n| - # find every (new, unique) member entry - depth_subentries = entries.each_with_object([]) do |entry, depth_entries| - submembers = entry["member"] + # track found groups + groups.each { |g| found[g.dn] = g } - # skip any members we've already found - submembers.reject! { |dn| found.key?(dn) } + # recursively find subgroups + unless groups.empty? + depth.times do |n| + # pull out subgroups' member DNs to search through + sub_dns = groups.each_with_object([]) do |subgroup, sub_dns| + sub_dns.concat member_dns(subgroup) + end - # find members of subgroup, including subgroups (N queries) - subentries = member_entries(entry) - next if subentries.empty? + # filter out if already searched for + sub_dns.reject! { |dn| searched.include?(dn) } - # track found subentries - subentries.each { |entry| found[entry.dn] = entry } + # give up if there's nothing else to search for + break if sub_dns.empty? - # collect all entries for this depth - depth_entries.concat subentries - end + # search for subgroups + subgroups = sub_dns.each_with_object([]) do |dn, subgroups| + subgroups.concat find_groups_by_dn(dn) + searched << dn + end - # stop if there are no more subgroups to search - break if depth_subentries.empty? + # give up if there were no subgroups found + break if subgroups.empty? - # go one level deeper - entries = depth_subentries + # track found subgroups + subgroups.each { |g| found[g.dn] = g } + + # descend another level + groups = subgroups + end end - # return all found entries - found.values - end + # entries to return + entries = [] - # Internal: Fetch member entries, including subgroups, for the given - # entry. - # - # Returns an Array of Net::LDAP::Entry objects. - def member_entries(entry) - entries = [] - dns = member_dns(entry) - uids = member_uids(entry) + # collect all member DNs, discarding dupes and subgroup DNs + members = found.values.each_with_object([]) do |group, dns| + entries << group + dns.concat member_dns(group) + end.uniq.reject { |dn| found.key?(dn) } - entries.concat entries_by_uid(uids) unless uids.empty? - entries.concat entries_by_dn(dns) unless dns.empty? + # wrap member DNs in Net::LDAP::Entry objects + entries.concat members.map! { |dn| Net::LDAP::Entry.new(dn) } entries end - private :member_entries - # Internal: Bind a list of DNs to their respective entries. + # Internal: Search for Groups by DN. # - # Returns an Array of Net::LDAP::Entry objects. - def entries_by_dn(members) - members.map do |dn| - ldap.domain(dn).bind(attributes: attrs) - end.compact + # Given a Distinguished Name (DN) String value, find the Group entry + # that matches it. The DN may map to a `person` entry, but we want to + # filter those out. + # + # This will find zero or one entry most of the time, but it's not + # guaranteed so we account for the possibility of more. + # + # This method is intended to be used with `Array#concat` by the caller. + # + # Returns an Array of zero or more Net::LDAP::Entry objects. + def find_groups_by_dn(dn) + ldap.search \ + base: dn, + scope: Net::LDAP::SearchScope_BaseObject, + attributes: attrs, + filter: ALL_GROUPS_FILTER end - private :entries_by_dn + private :find_groups_by_dn # Internal: Fetch entries by UID. # diff --git a/lib/github/ldap/membership_validators/active_directory.rb b/lib/github/ldap/membership_validators/active_directory.rb index 0c531c4..ff4e4fc 100644 --- a/lib/github/ldap/membership_validators/active_directory.rb +++ b/lib/github/ldap/membership_validators/active_directory.rb @@ -24,14 +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 - matched.map(&:dn).include?(entry.dn) + # Active Directory DNs are case-insensitive + 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/membership_validators/base.rb b/lib/github/ldap/membership_validators/base.rb index 3c47853..be378d1 100644 --- a/lib/github/ldap/membership_validators/base.rb +++ b/lib/github/ldap/membership_validators/base.rb @@ -13,9 +13,11 @@ class Base # # - ldap: GitHub::Ldap object # - groups: Array of Net::LDAP::Entry group objects - def initialize(ldap, groups) - @ldap = ldap - @groups = groups + # - options: Hash of options + def initialize(ldap, groups, options = {}) + @ldap = ldap + @groups = groups + @options = options end # Abstract: Performs the membership validation check. diff --git a/lib/github/ldap/membership_validators/recursive.rb b/lib/github/ldap/membership_validators/recursive.rb index 8c40aeb..3b78545 100644 --- a/lib/github/ldap/membership_validators/recursive.rb +++ b/lib/github/ldap/membership_validators/recursive.rb @@ -21,7 +21,31 @@ class Recursive < Base DEFAULT_MAX_DEPTH = 9 ATTRS = %w(dn cn) - def perform(entry, depth = DEFAULT_MAX_DEPTH) + # Internal: The maximum depth to search for membership. + attr_reader :depth + + # Public: Instantiate new search strategy. + # + # - ldap: GitHub::Ldap object + # - groups: Array of Net::LDAP::Entry group objects + # - options: Hash of options + # depth: Integer limit of recursion + # + # NOTE: This overrides default behavior to configure `depth`. + def initialize(ldap, groups, options = {}) + super + @depth = options[:depth] || DEFAULT_MAX_DEPTH + end + + def perform(entry, depth_override = nil) + if depth_override + warn "DEPRECATION WARNING: Calling Recursive#perform with a second argument is deprecated." + warn "Usage:" + warn " strategy = GitHub::Ldap::MembershipValidators::Recursive.new \\" + warn " ldap, depth: 5" + warn " strategy#perform(entry)" + end + # short circuit validation if there are no groups to check against return true if groups.empty? @@ -36,7 +60,7 @@ def perform(entry, depth = DEFAULT_MAX_DEPTH) next if membership.empty? # recurse to at most `depth` - depth.times do |n| + (depth_override || depth).times do |n| # find groups whose members include membership groups membership = domain.search(filter: membership_filter(membership), attributes: ATTRS) 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 797f716..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 @@ -224,3 +233,16 @@ def test_membership_for_posixGroups "Expected `#{@cn}` to not include the member `#{user.dn}`" end end + +class GitHubLdapActiveDirectoryGroupsTest < GitHub::Ldap::Test + def run(*) + return super if self.class.test_env == "activedirectory" + Minitest::Result.from(self) + end + + def test_filter_groups + domain = GitHub::Ldap.new(options).domain("DC=ad,DC=ghe,DC=local") + results = domain.filter_groups("ghe-admins") + assert_equal 1, results.size + end +end diff --git a/test/filter_test.rb b/test/filter_test.rb index 58992a8..4da83c9 100644 --- a/test/filter_test.rb +++ b/test/filter_test.rb @@ -1,6 +1,6 @@ require_relative 'test_helper' -class FilterTest < Minitest::Test +class FilterTest < GitHub::Ldap::Test class Subject include GitHub::Ldap::Filter def initialize(ldap) @@ -16,7 +16,7 @@ def [](field) end def setup - @ldap = GitHub::Ldap.new(:uid => 'uid') + @ldap = GitHub::Ldap.new(options.merge(:uid => 'uid')) @subject = Subject.new(@ldap) @me = 'uid=calavera,dc=github,dc=com' @uid = "calavera" diff --git a/test/ldap_test.rb b/test/ldap_test.rb index 48511d8..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 @@ -80,7 +108,7 @@ def test_search_strategy_defaults def test_search_strategy_detects_active_directory caps = Net::LDAP::Entry.new - caps[:supportedcapabilities] = [GitHub::Ldap::ACTIVE_DIRECTORY_V61_R2_OID] + caps[:supportedcapabilities] = [GitHub::Ldap::ACTIVE_DIRECTORY_V51_OID] @ldap.stub :capabilities, caps do @ldap.configure_search_strategy :detect @@ -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 0caafe2..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 @@ -123,4 +125,13 @@ def test_validates_user_in_posix_group validator = make_validator(%w(posix-group1)) assert validator.perform(@entry) end + + def test_validates_user_in_group_with_differently_cased_dn + validator = make_validator(%w(all-users)) + @entry[:dn].map(&:upcase!) + assert validator.perform(@entry) + + @entry[:dn].map(&:downcase!) + assert validator.perform(@entry) + end end diff --git a/test/membership_validators/recursive_test.rb b/test/membership_validators/recursive_test.rb index e351532..072ffca 100644 --- a/test/membership_validators/recursive_test.rb +++ b/test/membership_validators/recursive_test.rb @@ -8,9 +8,9 @@ def setup @validator = GitHub::Ldap::MembershipValidators::Recursive end - def make_validator(groups) + def make_validator(groups, options = {}) groups = @domain.groups(groups) - @validator.new(@ldap, groups) + @validator.new(@ldap, groups, options) end def test_validates_user_in_group @@ -34,8 +34,8 @@ def test_validates_user_in_great_grandchild_group end def test_does_not_validate_user_in_great_granchild_group_with_depth - validator = make_validator(%w(n-depth-nested-group3)) - refute validator.perform(@entry, 2) + validator = make_validator(%w(n-depth-nested-group3), depth: 2) + refute validator.perform(@entry) end def test_does_not_validate_user_not_in_group 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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy