Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
ecf5c43
Load a global catalog connection & add interface for searching it
Jul 25, 2016
51e72c0
Use global catalog to detect user, if server is Active Directory
Jul 25, 2016
203063d
Add tests for auth & unauth default Global Catalog settings
Jul 25, 2016
a23ccef
Test format of Global Catalog search results
Jul 25, 2016
c03c0de
Split up auth'd, unauth'd and global default settings tests
Jul 25, 2016
d4ae1ab
Updated documentation
Jul 25, 2016
8244f7c
Only initialize global catalog if server is Active Directory
Jul 25, 2016
3cf1c5a
Make capabilities public so domain can decide whether it's Active Dir
Jul 25, 2016
baef44d
Test for default instrumentation service on Global Catalog
Jul 25, 2016
e05f40c
Added doc for global_catalog_search
Jul 25, 2016
6125bc5
Test for domain to use global catalog if it's Active Directory
Jul 25, 2016
a34264b
Test for using default search on non-Active Directory servers
Jul 25, 2016
a00750f
Keep reference to credentials & instrumentation service for Global Ca…
Jul 25, 2016
6787585
Updates to Domain#user? & use of Global Catalog
Jul 25, 2016
c38ffca
Test that global catalog search uses empty base DN
Jul 25, 2016
ab2d49a
Set auth method for Global Catalog explicitly to :simple
Jul 25, 2016
3af0a88
Make sure global catalog search returns first entry from result array
Jul 25, 2016
dd24ee7
Document reason for auth references; remove redundant ivar
Jul 25, 2016
8d9eca3
Update doc
Jul 25, 2016
76db68a
Added test group to Gemfile
Jul 25, 2016
d69b0ba
Use assert_nil
Jul 25, 2016
9e9f9e9
Drop message in test
Jul 25, 2016
ad67b78
Created new ActiveDirectory user search class; moved tests
Aug 1, 2016
73ddf22
Added default strategy class for UserSearch
Aug 4, 2016
3d73e87
Load the default user search class; use strategy for Domain#user?
Aug 4, 2016
3f1838c
Override search for AD user search
Aug 4, 2016
115abb7
Update tests with new method signature for UserSearch#perform
Aug 4, 2016
f550ce5
Configure user search strategy
Aug 4, 2016
32840bc
Removed dead code -- was moved to its own class
Aug 4, 2016
89eca1e
More tests for user search strategy
Aug 4, 2016
faf1a16
Reverse the merge order for default search
Aug 4, 2016
2538aa0
Updated domain tests since adding user search strategy
Aug 4, 2016
d759075
Removed unnecessary tests
Aug 4, 2016
1ab77c0
Don't need this test
Aug 4, 2016
2e33e8b
Clean up requires
Aug 4, 2016
cf59b4c
Created a GlobalCatalog object; expose :connection on LDAP
Aug 4, 2016
0b22add
Configure user search consistent with other config strategies
Aug 4, 2016
5f9e3d7
Make active_directory_capability? private again
Aug 4, 2016
0ea4a93
Use mock utility for mock objects; Don't stub :new on Net::LDAP
Aug 4, 2016
18eb399
Test auth on user search strategy; minor test cleanup
Aug 4, 2016
32222d5
Updated documentation
Aug 4, 2016
5012000
Explictly override options for setting the base DN to ""
Aug 4, 2016
a522641
better hash structure
Aug 4, 2016
a3163fe
Updated documentation
Aug 4, 2016
c3ac0b3
Don't fall back on default user search for non-AD controllers
Aug 4, 2016
4154214
Make search & options private on ActiveDirectory user search
Aug 4, 2016
3da33e4
Make global connection interface private
Aug 4, 2016
735a42b
update documentation
Aug 4, 2016
3bc3979
Remove unneded test & condition
Aug 4, 2016
c05c4d7
First draft of referral chasing: set up referral connection properties
Jul 26, 2016
9afe164
WIP: Added referral chasing method, will move to GitHub::Ldap
Jul 26, 2016
1fce717
Moved chase_referral to ldap class
Jul 28, 2016
526d63a
Cache new referral connections as we go
Jul 28, 2016
82704e5
removed old chase_referral method
Jul 28, 2016
022d455
Add method to reset base dn on search filter for use with referrals
Jul 28, 2016
652633b
Reset base dn on filter before searcing on referral controller
Jul 28, 2016
bb149ca
cleanup
Jul 29, 2016
218675b
Use GitHub::Ldap instead of Net::LDAP for referral connections
Jul 29, 2016
971698c
Don't reset base_dn, will pass in FQDN for groups from client
Jul 29, 2016
990cca8
No need for DN matcher
Jul 29, 2016
54defdb
Create a ConnectionPool object to encapsulate caching connection objects
Jul 29, 2016
4b0ccff
Split connection pool tests
Jul 29, 2016
edd8107
A little cleanup
Jul 29, 2016
5d7c294
Abstracted referral chasing into its own class
Jul 29, 2016
96ba488
load referral_chaser; exposed admin user & pw for referrals to use
Jul 29, 2016
9edc1c5
Removed dead code
Jul 29, 2016
7f3e6f2
Use new referral chaser class in ActiveDirectory validator
Jul 29, 2016
b7da6e7
Pushing referral aggregation from callsite into ReferallChaser
Jul 30, 2016
704ea39
Use base connection's port as the default port
Aug 1, 2016
8199555
Use ReferralChaser to do all the search heavy lifting for AD
Aug 1, 2016
d3dc5c4
Updated documentation
Aug 1, 2016
644fb68
Updated tests
Aug 1, 2016
e759522
Added a GitHub::Ldap::URL object to encapsulate parsing ldap urls
Aug 1, 2016
5cefd97
Remove redundant test
Aug 1, 2016
0ac330a
Only iterate over Referall type entries in ReferralChaser
Aug 1, 2016
1a95540
memoize the referral chaser
Aug 2, 2016
e2c3675
check entry for nil when collecting referrals
Aug 2, 2016
a4573d7
Remove test file; clean up merge
Aug 4, 2016
3eddd8b
A bit more merge cleanup
Aug 4, 2016
7cdbe86
Minor style/cleanup
Aug 4, 2016
80a6127
Don't need result reference
Aug 4, 2016
f052559
Better format for private method
Aug 4, 2016
77b4209
Add host interface to ldap; updated tests
Aug 4, 2016
518bd85
Moved connection cache to its own class
Aug 4, 2016
27cf716
move mocha requirement to test_helper
Aug 4, 2016
ef20459
Better use of mocks
Aug 5, 2016
51f793c
Fix mock for referral_chaser_test
Aug 5, 2016
a0f1f24
Fixing CI: don't use a real connection
Aug 5, 2016
88319aa
CI Fixes: move all setup code inline
Aug 5, 2016
cc3fd3a
Removing ruby 1.9.3 from CI
Aug 5, 2016
85ab9f8
Use correct port for LDAP connection
Aug 5, 2016
ed66f8d
CI Fixes: use the right credentials to connect to test ldap server
Aug 5, 2016
6ad401a
CI Fixes: use correct connection attrs for tests
Aug 5, 2016
142e3fd
Test Ruby 2.0
mtodd Aug 5, 2016
64c6e08
Configure connection with GitHub::Ldap::Test#options
mtodd Aug 5, 2016
2b80058
Rename mock_connection to ldap
mtodd Aug 5, 2016
e7de8e7
Setup test hosts for connection caching
mtodd Aug 5, 2016
02cef0b
Document user search strategy callsite
Aug 5, 2016
15990be
Test for invalid URL strings as well as bad schemes in ReferralChaser
Aug 5, 2016
60e7b7f
Fixing some merge-fu: use @ldap not @mock_connection
Aug 5, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ gemspec
group :test, :development do
gem "byebug", :platforms => [:mri_20, :mri_21]
end

group :test do
gem "mocha"
end
44 changes: 43 additions & 1 deletion lib/github/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
#
Expand All @@ -69,6 +80,11 @@ 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],
Expand Down Expand Up @@ -98,6 +114,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
Expand Down Expand Up @@ -281,6 +300,29 @@ 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 = begin
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
end

# Internal: Configure the member search strategy.
#
#
Expand Down
26 changes: 26 additions & 0 deletions lib/github/ldap/connection_cache.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 1 addition & 4 deletions lib/github/ldap/domain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 10 additions & 2 deletions lib/github/ldap/membership_validators/active_directory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
98 changes: 98 additions & 0 deletions lib/github/ldap/referral_chaser.rb
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the port specified in the referral URL be preferred?

Copy link
Contributor Author

@davesims davesims Aug 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so we should prefer the user config. Ruby's URI (which is what's underlying GitHub::Ldap::URL) gives a default port of 389 because it's smart about the protocol, ldap. I've yet to see a referral come back with a port on the URL, so I think in this case we do want to prefer the port designated by the user, which would not be 389 and would instead by 686 if they're using TLS/LDAPS.

That said, I just tested URI in pry, and it's also smart about using 686 if the protocol is ldaps. If I knew that the referral URLs were consistent about the protocol I'd say let's prefer the port returned by the referral. I'll set up some test conditions on my local AD forest & find out.

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 changes: 87 additions & 0 deletions lib/github/ldap/url.rb
Original file line number Diff line number Diff line change
@@ -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

Loading