-
Notifications
You must be signed in to change notification settings - Fork 27
MemberSearch cleanup and polish #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
12ba663
e79d632
a2ea06f
05cb39c
4f774fd
d4d705d
a9b86ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
module GitHub | ||
class Ldap | ||
module Capabilities | ||
# Internal: The capability required to use the ActiveDirectory strategy. | ||
# See: http://msdn.microsoft.com/en-us/library/cc223359.aspx. | ||
ACTIVE_DIRECTORY_V61_R2_OID = "1.2.840.113556.1.4.2080".freeze | ||
|
||
# Internal: Detect whether the LDAP host is an ActiveDirectory server. | ||
# | ||
# See: http://msdn.microsoft.com/en-us/library/cc223359.aspx. | ||
# | ||
# Returns true if the host is an ActiveDirectory server, false otherwise. | ||
def active_directory_capability? | ||
capabilities[:supportedcapabilities].include?(ACTIVE_DIRECTORY_V61_R2_OID) | ||
end | ||
|
||
# Internal: Returns the Net::LDAP::Entry object describing the LDAP | ||
# host's capabilities (via the Root DSE). | ||
def capabilities | ||
ldap.capabilities | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
require 'github/ldap/member_search/base' | ||
require 'github/ldap/member_search/detect' | ||
require 'github/ldap/member_search/classic' | ||
require 'github/ldap/member_search/recursive' | ||
require 'github/ldap/member_search/active_directory' | ||
|
||
module GitHub | ||
class Ldap | ||
# Provides various strategies for member lookup. | ||
# | ||
# For example: | ||
# | ||
# group = domain.groups(%w(Engineering)).first | ||
# strategy = GitHub::Ldap::MemberSearch::Recursive.new(ldap) | ||
# strategy.perform(group) #=> [#<Net::LDAP::Entry>] | ||
# | ||
module MemberSearch | ||
# Internal: Mapping of strategy name to class. | ||
STRATEGIES = { | ||
:classic => GitHub::Ldap::MemberSearch::Classic, | ||
:recursive => GitHub::Ldap::MemberSearch::Recursive, | ||
:active_directory => GitHub::Ldap::MemberSearch::ActiveDirectory | ||
} | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
module GitHub | ||
class Ldap | ||
module MemberSearch | ||
# Look up group members using the ActiveDirectory "in chain" matching rule. | ||
# | ||
# The 1.2.840.113556.1.4.1941 matching rule (LDAP_MATCHING_RULE_IN_CHAIN) | ||
# "walks the chain of ancestry in objects all the way to the root until | ||
# it finds a match". | ||
# Source: http://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx | ||
# | ||
# This means we have an efficient method of searching for group members, | ||
# even in nested groups, performed on the server side. | ||
class ActiveDirectory < Base | ||
OID = "1.2.840.113556.1.4.1941" | ||
|
||
# Internal: The default attributes to query for. | ||
# NOTE: We technically don't need any by default, but if we left this | ||
# empty, we'd be querying for *all* attributes which is less ideal. | ||
DEFAULT_ATTRS = %w(objectClass) | ||
|
||
# Internal: The attributes to search for. | ||
attr_reader :attrs | ||
|
||
# Public: Instantiate new search strategy. | ||
# | ||
# - ldap: GitHub::Ldap object | ||
# - options: Hash of options | ||
# | ||
# NOTE: This overrides default behavior to configure attrs`. | ||
def initialize(ldap, options = {}) | ||
super | ||
@attrs = Array(options[:attrs]).concat DEFAULT_ATTRS | ||
end | ||
|
||
# Public: Performs search for group members, including groups and | ||
# members of subgroups, using ActiveDirectory's "in chain" matching | ||
# rule. | ||
# | ||
# Returns Array of Net::LDAP::Entry objects. | ||
def perform(group) | ||
filter = member_of_in_chain_filter(group) | ||
|
||
# search for all members of the group, including subgroups, by | ||
# searching "in chain". | ||
domains.each_with_object([]) do |domain, members| | ||
members.concat domain.search(filter: filter, attributes: attrs) | ||
end | ||
end | ||
|
||
# Internal: Constructs a member filter using the "in chain" | ||
# extended matching rule afforded by ActiveDirectory. | ||
# | ||
# Returns a Net::LDAP::Filter object. | ||
def member_of_in_chain_filter(entry) | ||
Net::LDAP::Filter.ex("memberOf:#{OID}", entry.dn) | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
module GitHub | ||
class Ldap | ||
module MemberSearch | ||
class Base | ||
|
||
# Internal: The GitHub::Ldap object to search domains with. | ||
attr_reader :ldap | ||
|
||
# Public: Instantiate new search strategy. | ||
# | ||
# - ldap: GitHub::Ldap object | ||
# - options: Hash of options | ||
def initialize(ldap, options = {}) | ||
@ldap = ldap | ||
@options = options | ||
end | ||
|
||
# Public: Abstract: Performs search for group members. | ||
# | ||
# Returns Array of Net::LDAP::Entry objects. | ||
# def perform(entry) | ||
# end | ||
|
||
# Internal: Domains to search through. | ||
# | ||
# Returns an Array of GitHub::Ldap::Domain objects. | ||
def domains | ||
@domains ||= ldap.search_domains.map { |base| ldap.domain(base) } | ||
end | ||
private :domains | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
module GitHub | ||
class Ldap | ||
module MemberSearch | ||
# Detects the LDAP host's capabilities and determines the appropriate | ||
# member search strategy at runtime. | ||
# | ||
# Currently detects for ActiveDirectory in-chain membership validation. | ||
# | ||
# An explicit strategy can also be defined via | ||
# `GitHub::Ldap#member_search_strategy=`. | ||
# | ||
# See also `GitHub::Ldap#configure_member_search_strategy`. | ||
class Detect | ||
# Defines `active_directory_capability?` and necessary helpers. | ||
include GitHub::Ldap::Capabilities | ||
|
||
# Internal: The GitHub::Ldap object to search domains with. | ||
attr_reader :ldap | ||
|
||
# Internal: The Hash of options to pass through to the strategy. | ||
attr_reader :options | ||
|
||
# Public: Instantiate a meta strategy to detect the right strategy | ||
# to use for the search, and call that strategy, at runtime. | ||
# | ||
# - ldap: GitHub::Ldap object | ||
# - options: Hash of options (passed through) | ||
def initialize(ldap, options = {}) | ||
@ldap = ldap | ||
@options = options | ||
end | ||
|
||
# Public: Performs search for group members via the appropriate search | ||
# strategy detected/configured. | ||
# | ||
# Returns Array of Net::LDAP::Entry objects. | ||
def perform(entry) | ||
strategy.perform(entry) | ||
end | ||
|
||
# Internal: Returns the member search strategy object. | ||
def strategy | ||
@strategy ||= begin | ||
strategy = detect_strategy | ||
strategy.new(ldap, options) | ||
end | ||
end | ||
|
||
# Internal: Find the most appropriate search strategy, either by | ||
# configuration or by detecting the host's capabilities. | ||
# | ||
# Returns the strategy class. | ||
def detect_strategy | ||
case | ||
when GitHub::Ldap::MemberSearch::STRATEGIES.key?(strategy_config) | ||
GitHub::Ldap::MemberSearch::STRATEGIES[strategy_config] | ||
when active_directory_capability? | ||
GitHub::Ldap::MemberSearch::STRATEGIES[:active_directory] | ||
else | ||
GitHub::Ldap::MemberSearch::STRATEGIES[:recursive] | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This case statement digs into the implementation of the other strategies too much. One indicator is the def detect_strategy
# try to use the most efficient strategy first
[ActiveDirectory, Recursive, Classic].detect do |strategy|
strategy.handles?(ldap.capabilities) # strategy is responsible for capability detection
end
end
def configure_member_search_strategy(strategy = nil)
GitHub::Ldap::MemberSearch::STRATEGIES[strategy] || GitHub::Ldap::MemberSearch::Detect
end There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jch really like these recommendations! This was definitely the kind of feedback I feel was most needed, thanks. I think these can be tackled in a follow up PR, though, and it would be better to keep this PR focused and consistent with what's already in master before attempting these changes. That sound like a solid approach? Or is there something here that is a blocking concern? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No blocking concern, but I would like to see this done before the freeze for long term maintenance's sake. Your call on where you'd like it done. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jch yeah, the tasks I outlined below (#69 (comment)) are meant to be done before 1.6.0 gets cut. Just wanted to split up the work into separate PRs to keep focus better. |
||
end | ||
|
||
# Internal: Returns the configured member search strategy Symbol. | ||
def strategy_config | ||
ldap.member_search_strategy | ||
end | ||
end | ||
end | ||
end | ||
end |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This module feels like a premature extraction. It's specific to ActiveDirectory, and doesn't seem useful outside this context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jch agree in some senses, but there are two separate classes that depend on the same behavior that didn't feel right duplicating in (the ActiveDirectory MemberSearch and MembershipValidation strategies). It's a mix of AD-specific feature detection with general capability exposure.
I'm not crazy about it, but it unblocks this PR and can be easily changed in the future when a better approach can be considered, particularly because it's internal only. I think this need will diminish with the changes you outline in #69 (comment), so it is likely temporary.
Thoughts?