Skip to content

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

Merged
merged 7 commits into from
Dec 5, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 25 additions & 1 deletion lib/github/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class Ldap
require 'github/ldap/virtual_group'
require 'github/ldap/virtual_attributes'
require 'github/ldap/instrumentation'
require 'github/ldap/members'
require 'github/ldap/capabilities'
require 'github/ldap/member_search'
require 'github/ldap/membership_validators'

include Instrumentation
Expand All @@ -36,6 +37,7 @@ class Ldap

attr_reader :uid, :search_domains, :virtual_attributes,
:membership_validator,
:member_search_strategy,
:instrumentation_service

# Build a new GitHub::Ldap instance
Expand Down Expand Up @@ -92,6 +94,9 @@ def initialize(options = {})
# configure which strategy should be used to validate user membership
configure_membership_validation_strategy(options[:membership_validator])

# configure which strategy should be used for member search
configure_member_search_strategy(options[:member_search_strategy])

# enables instrumenting queries
@instrumentation_service = options[:instrumentation_service]
end
Expand Down Expand Up @@ -255,5 +260,24 @@ def configure_membership_validation_strategy(strategy = nil)
:detect
end
end

# Internal: Configure the member search strategy.
#
# Used by GitHub::Ldap::MemberSearch::Detect to force a specific strategy
# (instead of detecting the host capabilities and deciding at runtime).
#
# If `strategy` is not provided, or doesn't match a known strategy,
# defaults to `:detect`. Otherwise the configured strategy is selected.
#
# Returns the selected strategy Symbol.
def configure_member_search_strategy(strategy = nil)
@member_search_strategy =
case strategy.to_s
when "classic", "recursive"
strategy.to_sym
else
:detect
end
end
end
end
24 changes: 24 additions & 0 deletions lib/github/ldap/capabilities.rb
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
26 changes: 26 additions & 0 deletions lib/github/ldap/member_search.rb
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
60 changes: 60 additions & 0 deletions lib/github/ldap/member_search/active_directory.rb
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
34 changes: 34 additions & 0 deletions lib/github/ldap/member_search/base.rb
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
@@ -1,21 +1,9 @@
module GitHub
class Ldap
module Members
module MemberSearch
# Look up group members using the existing `Group#members` and
# `Group#subgroups` API.
class Classic
# 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 (unused)
def initialize(ldap, options = {})
@ldap = ldap
@options = options
end

class Classic < Base
# Public: Performs search for group members, including groups and
# members of subgroups recursively.
#
Expand Down
71 changes: 71 additions & 0 deletions lib/github/ldap/member_search/detect.rb
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
Copy link
Contributor

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.

Copy link
Member Author

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?


# 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 active_directory_capability?, making this class also know about AD. Spitballing here, but I'd like to see something like:

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

LDAP#configure_member_search_strategy would be responsible for handling when a user sets an explicit strategy, but default to Detect to handle all other cases:

def configure_member_search_strategy(strategy = nil)
    GitHub::Ldap::MemberSearch::STRATEGIES[strategy] || GitHub::Ldap::MemberSearch::Detect
end

Copy link
Member Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
module GitHub
class Ldap
module Members
module MemberSearch
# Look up group members recursively.
#
# This results in a maximum of `depth` iterations/recursions to look up
# members of a group and its subgroups.
class Recursive
class Recursive < Base
include Filter

DEFAULT_MAX_DEPTH = 9
DEFAULT_ATTRS = %w(member uniqueMember memberUid)

# Internal: The GitHub::Ldap object to search domains with.
attr_reader :ldap

# Internal: The maximum depth to search for members.
attr_reader :depth

Expand All @@ -24,11 +21,12 @@ class Recursive
#
# - ldap: GitHub::Ldap object
# - options: Hash of options
#
# NOTE: This overrides default behavior to configure `depth` and `attrs`.
def initialize(ldap, options = {})
@ldap = ldap
@options = options
@depth = options[:depth] || DEFAULT_MAX_DEPTH
@attrs = Array(options[:attrs]).concat DEFAULT_ATTRS
super
@depth = options[:depth] || DEFAULT_MAX_DEPTH
@attrs = Array(options[:attrs]).concat DEFAULT_ATTRS
end

# Public: Performs search for group members, including groups and
Expand Down Expand Up @@ -129,14 +127,6 @@ def member_uids(entry)
entry["memberUid"]
end
private :member_uids

# 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
Expand Down
22 changes: 0 additions & 22 deletions lib/github/ldap/members.rb

This file was deleted.

Loading