Skip to content

Commit 3a94c85

Browse files
committed
Merge pull request #69 from github/member-search-cleanup
MemberSearch cleanup and polish
2 parents 51f9cfd + a9b86ef commit 3a94c85

File tree

15 files changed

+379
-80
lines changed

15 files changed

+379
-80
lines changed

lib/github/ldap.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ class Ldap
99
require 'github/ldap/virtual_group'
1010
require 'github/ldap/virtual_attributes'
1111
require 'github/ldap/instrumentation'
12-
require 'github/ldap/members'
12+
require 'github/ldap/capabilities'
13+
require 'github/ldap/member_search'
1314
require 'github/ldap/membership_validators'
1415

1516
include Instrumentation
@@ -36,6 +37,7 @@ class Ldap
3637

3738
attr_reader :uid, :search_domains, :virtual_attributes,
3839
:membership_validator,
40+
:member_search_strategy,
3941
:instrumentation_service
4042

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

97+
# configure which strategy should be used for member search
98+
configure_member_search_strategy(options[:member_search_strategy])
99+
95100
# enables instrumenting queries
96101
@instrumentation_service = options[:instrumentation_service]
97102
end
@@ -255,5 +260,24 @@ def configure_membership_validation_strategy(strategy = nil)
255260
:detect
256261
end
257262
end
263+
264+
# Internal: Configure the member search strategy.
265+
#
266+
# Used by GitHub::Ldap::MemberSearch::Detect to force a specific strategy
267+
# (instead of detecting the host capabilities and deciding at runtime).
268+
#
269+
# If `strategy` is not provided, or doesn't match a known strategy,
270+
# defaults to `:detect`. Otherwise the configured strategy is selected.
271+
#
272+
# Returns the selected strategy Symbol.
273+
def configure_member_search_strategy(strategy = nil)
274+
@member_search_strategy =
275+
case strategy.to_s
276+
when "classic", "recursive"
277+
strategy.to_sym
278+
else
279+
:detect
280+
end
281+
end
258282
end
259283
end

lib/github/ldap/capabilities.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module GitHub
2+
class Ldap
3+
module Capabilities
4+
# Internal: The capability required to use the ActiveDirectory strategy.
5+
# See: http://msdn.microsoft.com/en-us/library/cc223359.aspx.
6+
ACTIVE_DIRECTORY_V61_R2_OID = "1.2.840.113556.1.4.2080".freeze
7+
8+
# Internal: Detect whether the LDAP host is an ActiveDirectory server.
9+
#
10+
# See: http://msdn.microsoft.com/en-us/library/cc223359.aspx.
11+
#
12+
# Returns true if the host is an ActiveDirectory server, false otherwise.
13+
def active_directory_capability?
14+
capabilities[:supportedcapabilities].include?(ACTIVE_DIRECTORY_V61_R2_OID)
15+
end
16+
17+
# Internal: Returns the Net::LDAP::Entry object describing the LDAP
18+
# host's capabilities (via the Root DSE).
19+
def capabilities
20+
ldap.capabilities
21+
end
22+
end
23+
end
24+
end

lib/github/ldap/member_search.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require 'github/ldap/member_search/base'
2+
require 'github/ldap/member_search/detect'
3+
require 'github/ldap/member_search/classic'
4+
require 'github/ldap/member_search/recursive'
5+
require 'github/ldap/member_search/active_directory'
6+
7+
module GitHub
8+
class Ldap
9+
# Provides various strategies for member lookup.
10+
#
11+
# For example:
12+
#
13+
# group = domain.groups(%w(Engineering)).first
14+
# strategy = GitHub::Ldap::MemberSearch::Recursive.new(ldap)
15+
# strategy.perform(group) #=> [#<Net::LDAP::Entry>]
16+
#
17+
module MemberSearch
18+
# Internal: Mapping of strategy name to class.
19+
STRATEGIES = {
20+
:classic => GitHub::Ldap::MemberSearch::Classic,
21+
:recursive => GitHub::Ldap::MemberSearch::Recursive,
22+
:active_directory => GitHub::Ldap::MemberSearch::ActiveDirectory
23+
}
24+
end
25+
end
26+
end
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
module GitHub
2+
class Ldap
3+
module MemberSearch
4+
# Look up group members using the ActiveDirectory "in chain" matching rule.
5+
#
6+
# The 1.2.840.113556.1.4.1941 matching rule (LDAP_MATCHING_RULE_IN_CHAIN)
7+
# "walks the chain of ancestry in objects all the way to the root until
8+
# it finds a match".
9+
# Source: http://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx
10+
#
11+
# This means we have an efficient method of searching for group members,
12+
# even in nested groups, performed on the server side.
13+
class ActiveDirectory < Base
14+
OID = "1.2.840.113556.1.4.1941"
15+
16+
# Internal: The default attributes to query for.
17+
# NOTE: We technically don't need any by default, but if we left this
18+
# empty, we'd be querying for *all* attributes which is less ideal.
19+
DEFAULT_ATTRS = %w(objectClass)
20+
21+
# Internal: The attributes to search for.
22+
attr_reader :attrs
23+
24+
# Public: Instantiate new search strategy.
25+
#
26+
# - ldap: GitHub::Ldap object
27+
# - options: Hash of options
28+
#
29+
# NOTE: This overrides default behavior to configure attrs`.
30+
def initialize(ldap, options = {})
31+
super
32+
@attrs = Array(options[:attrs]).concat DEFAULT_ATTRS
33+
end
34+
35+
# Public: Performs search for group members, including groups and
36+
# members of subgroups, using ActiveDirectory's "in chain" matching
37+
# rule.
38+
#
39+
# Returns Array of Net::LDAP::Entry objects.
40+
def perform(group)
41+
filter = member_of_in_chain_filter(group)
42+
43+
# search for all members of the group, including subgroups, by
44+
# searching "in chain".
45+
domains.each_with_object([]) do |domain, members|
46+
members.concat domain.search(filter: filter, attributes: attrs)
47+
end
48+
end
49+
50+
# Internal: Constructs a member filter using the "in chain"
51+
# extended matching rule afforded by ActiveDirectory.
52+
#
53+
# Returns a Net::LDAP::Filter object.
54+
def member_of_in_chain_filter(entry)
55+
Net::LDAP::Filter.ex("memberOf:#{OID}", entry.dn)
56+
end
57+
end
58+
end
59+
end
60+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module GitHub
2+
class Ldap
3+
module MemberSearch
4+
class Base
5+
6+
# Internal: The GitHub::Ldap object to search domains with.
7+
attr_reader :ldap
8+
9+
# Public: Instantiate new search strategy.
10+
#
11+
# - ldap: GitHub::Ldap object
12+
# - options: Hash of options
13+
def initialize(ldap, options = {})
14+
@ldap = ldap
15+
@options = options
16+
end
17+
18+
# Public: Abstract: Performs search for group members.
19+
#
20+
# Returns Array of Net::LDAP::Entry objects.
21+
# def perform(entry)
22+
# end
23+
24+
# Internal: Domains to search through.
25+
#
26+
# Returns an Array of GitHub::Ldap::Domain objects.
27+
def domains
28+
@domains ||= ldap.search_domains.map { |base| ldap.domain(base) }
29+
end
30+
private :domains
31+
end
32+
end
33+
end
34+
end
Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
11
module GitHub
22
class Ldap
3-
module Members
3+
module MemberSearch
44
# Look up group members using the existing `Group#members` and
55
# `Group#subgroups` API.
6-
class Classic
7-
# Internal: The GitHub::Ldap object to search domains with.
8-
attr_reader :ldap
9-
10-
# Public: Instantiate new search strategy.
11-
#
12-
# - ldap: GitHub::Ldap object
13-
# - options: Hash of options (unused)
14-
def initialize(ldap, options = {})
15-
@ldap = ldap
16-
@options = options
17-
end
18-
6+
class Classic < Base
197
# Public: Performs search for group members, including groups and
208
# members of subgroups recursively.
219
#
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
module GitHub
2+
class Ldap
3+
module MemberSearch
4+
# Detects the LDAP host's capabilities and determines the appropriate
5+
# member search strategy at runtime.
6+
#
7+
# Currently detects for ActiveDirectory in-chain membership validation.
8+
#
9+
# An explicit strategy can also be defined via
10+
# `GitHub::Ldap#member_search_strategy=`.
11+
#
12+
# See also `GitHub::Ldap#configure_member_search_strategy`.
13+
class Detect
14+
# Defines `active_directory_capability?` and necessary helpers.
15+
include GitHub::Ldap::Capabilities
16+
17+
# Internal: The GitHub::Ldap object to search domains with.
18+
attr_reader :ldap
19+
20+
# Internal: The Hash of options to pass through to the strategy.
21+
attr_reader :options
22+
23+
# Public: Instantiate a meta strategy to detect the right strategy
24+
# to use for the search, and call that strategy, at runtime.
25+
#
26+
# - ldap: GitHub::Ldap object
27+
# - options: Hash of options (passed through)
28+
def initialize(ldap, options = {})
29+
@ldap = ldap
30+
@options = options
31+
end
32+
33+
# Public: Performs search for group members via the appropriate search
34+
# strategy detected/configured.
35+
#
36+
# Returns Array of Net::LDAP::Entry objects.
37+
def perform(entry)
38+
strategy.perform(entry)
39+
end
40+
41+
# Internal: Returns the member search strategy object.
42+
def strategy
43+
@strategy ||= begin
44+
strategy = detect_strategy
45+
strategy.new(ldap, options)
46+
end
47+
end
48+
49+
# Internal: Find the most appropriate search strategy, either by
50+
# configuration or by detecting the host's capabilities.
51+
#
52+
# Returns the strategy class.
53+
def detect_strategy
54+
case
55+
when GitHub::Ldap::MemberSearch::STRATEGIES.key?(strategy_config)
56+
GitHub::Ldap::MemberSearch::STRATEGIES[strategy_config]
57+
when active_directory_capability?
58+
GitHub::Ldap::MemberSearch::STRATEGIES[:active_directory]
59+
else
60+
GitHub::Ldap::MemberSearch::STRATEGIES[:recursive]
61+
end
62+
end
63+
64+
# Internal: Returns the configured member search strategy Symbol.
65+
def strategy_config
66+
ldap.member_search_strategy
67+
end
68+
end
69+
end
70+
end
71+
end

lib/github/ldap/members/recursive.rb renamed to lib/github/ldap/member_search/recursive.rb

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
module GitHub
22
class Ldap
3-
module Members
3+
module MemberSearch
44
# Look up group members recursively.
55
#
66
# This results in a maximum of `depth` iterations/recursions to look up
77
# members of a group and its subgroups.
8-
class Recursive
8+
class Recursive < Base
99
include Filter
1010

1111
DEFAULT_MAX_DEPTH = 9
1212
DEFAULT_ATTRS = %w(member uniqueMember memberUid)
1313

14-
# Internal: The GitHub::Ldap object to search domains with.
15-
attr_reader :ldap
16-
1714
# Internal: The maximum depth to search for members.
1815
attr_reader :depth
1916

@@ -24,11 +21,12 @@ class Recursive
2421
#
2522
# - ldap: GitHub::Ldap object
2623
# - options: Hash of options
24+
#
25+
# NOTE: This overrides default behavior to configure `depth` and `attrs`.
2726
def initialize(ldap, options = {})
28-
@ldap = ldap
29-
@options = options
30-
@depth = options[:depth] || DEFAULT_MAX_DEPTH
31-
@attrs = Array(options[:attrs]).concat DEFAULT_ATTRS
27+
super
28+
@depth = options[:depth] || DEFAULT_MAX_DEPTH
29+
@attrs = Array(options[:attrs]).concat DEFAULT_ATTRS
3230
end
3331

3432
# Public: Performs search for group members, including groups and
@@ -129,14 +127,6 @@ def member_uids(entry)
129127
entry["memberUid"]
130128
end
131129
private :member_uids
132-
133-
# Internal: Domains to search through.
134-
#
135-
# Returns an Array of GitHub::Ldap::Domain objects.
136-
def domains
137-
@domains ||= ldap.search_domains.map { |base| ldap.domain(base) }
138-
end
139-
private :domains
140130
end
141131
end
142132
end

lib/github/ldap/members.rb

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)