Merge branch 'master' into use_global_catalog_for_auth

This commit is contained in:
Dave Sims 2016-08-05 10:40:29 -05:00
Родитель 3080212e1b 52b78e20e3
Коммит 3a58f8323b
17 изменённых файлов: 475 добавлений и 32 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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)

Просмотреть файл

@ -2,7 +2,7 @@
Gem::Specification.new do |spec|
spec.name = "github-ldap"
spec.version = "1.9.0"
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.11.0'
spec.add_dependency 'net-ldap', '~> 0.15.0'
spec.add_development_dependency "bundler", "~> 1.3"
spec.add_development_dependency 'ladle'

Просмотреть файл

@ -12,6 +12,9 @@ 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
@ -40,13 +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,
:user_search_strategy,
:connection
:connection,
:admin_user,
:admin_password,
:port
# Build a new GitHub::Ldap instance
#
@ -76,6 +83,7 @@ module GitHub
# 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],
@ -106,7 +114,7 @@ module GitHub
# configure both the membership validator and the member search strategies
configure_search_strategy(options[:search_strategy])
# configure both the membership validator and the member search strategies
# 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
@ -340,9 +348,6 @@ module GitHub
end
end
private
# Internal: Detect whether the LDAP host is an ActiveDirectory server.
#
# See: http://msdn.microsoft.com/en-us/library/cc223359.aspx.
@ -351,7 +356,6 @@ module GitHub
def active_directory_capability?
capabilities[:supportedcapabilities].include?(ACTIVE_DIRECTORY_V51_OID)
end
attr_reader :admin_user, :admin_password
private :active_directory_capability?
end
end

Просмотреть файл

@ -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

Просмотреть файл

@ -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"

Просмотреть файл

@ -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

87
lib/github/ldap/url.rb Normal file
Просмотреть файл

@ -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

Просмотреть файл

@ -37,7 +37,7 @@ module GitHub
auth = netldap.instance_variable_get(:@auth)
new({
host: ldap.instance_variable_get(:@host),
host: ldap.host,
instrumentation_service: ldap.instrumentation_service,
port: encryption ? LDAPS_GC_PORT : STANDARD_GC_PORT,
auth: auth,

Просмотреть файл

@ -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

Просмотреть файл

@ -1,5 +1,4 @@
require_relative 'test_helper'
require 'mocha/mini_test'
module GitHubLdapDomainTestCases
def setup
@ -143,8 +142,8 @@ module GitHubLdapDomainTestCases
end
def test_user_search_returns_first_entry
entry = Object.new
search_strategy = Object.new
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])

Просмотреть файл

@ -1,5 +1,4 @@
require_relative 'test_helper'
require 'mocha/mini_test'
module GitHubLdapTestCases
def setup

Просмотреть файл

@ -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

Просмотреть файл

@ -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

85
test/url_test.rb Normal file
Просмотреть файл

@ -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

Просмотреть файл

@ -1,46 +1,52 @@
require_relative '../test_helper'
require 'mocha/mini_test'
class GitHubLdapActiveDirectoryUserSearchTests < GitHub::Ldap::Test
def setup
@ldap = GitHub::Ldap.new(options)
@ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(@ldap)
end
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", {})
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", {})
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", {})
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
global_catalog = @ad_user_search.global_catalog_connection
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 "127.0.0.1", global_catalog.host
assert_equal "ghe.dev", global_catalog.host
assert_equal 3268, global_catalog.port
assert_equal "MockInstrumentationService", instrumentation_service.class.name
end

Просмотреть файл

@ -1,5 +1,4 @@
require_relative '../test_helper'
require 'mocha/mini_test'
class GitHubLdapActiveDirectoryUserSearchTests < GitHub::Ldap::Test
def setup