diff --git a/lib/github/ldap.rb b/lib/github/ldap.rb index 43c3f3b..dc18a44 100644 --- a/lib/github/ldap.rb +++ b/lib/github/ldap.rb @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/lib/github/ldap/capabilities.rb b/lib/github/ldap/capabilities.rb new file mode 100644 index 0000000..370be4b --- /dev/null +++ b/lib/github/ldap/capabilities.rb @@ -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 diff --git a/lib/github/ldap/member_search.rb b/lib/github/ldap/member_search.rb new file mode 100644 index 0000000..6269638 --- /dev/null +++ b/lib/github/ldap/member_search.rb @@ -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) #=> [#] + # + 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 diff --git a/lib/github/ldap/member_search/active_directory.rb b/lib/github/ldap/member_search/active_directory.rb new file mode 100644 index 0000000..f78085a --- /dev/null +++ b/lib/github/ldap/member_search/active_directory.rb @@ -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 diff --git a/lib/github/ldap/member_search/base.rb b/lib/github/ldap/member_search/base.rb new file mode 100644 index 0000000..e3491f1 --- /dev/null +++ b/lib/github/ldap/member_search/base.rb @@ -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 diff --git a/lib/github/ldap/members/classic.rb b/lib/github/ldap/member_search/classic.rb similarity index 54% rename from lib/github/ldap/members/classic.rb rename to lib/github/ldap/member_search/classic.rb index 81a5601..47bb7a1 100644 --- a/lib/github/ldap/members/classic.rb +++ b/lib/github/ldap/member_search/classic.rb @@ -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. # diff --git a/lib/github/ldap/member_search/detect.rb b/lib/github/ldap/member_search/detect.rb new file mode 100644 index 0000000..06cdb32 --- /dev/null +++ b/lib/github/ldap/member_search/detect.rb @@ -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 + end + + # Internal: Returns the configured member search strategy Symbol. + def strategy_config + ldap.member_search_strategy + end + end + end + end +end diff --git a/lib/github/ldap/members/recursive.rb b/lib/github/ldap/member_search/recursive.rb similarity index 87% rename from lib/github/ldap/members/recursive.rb rename to lib/github/ldap/member_search/recursive.rb index 9b0becc..5fe2489 100644 --- a/lib/github/ldap/members/recursive.rb +++ b/lib/github/ldap/member_search/recursive.rb @@ -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 @@ -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 @@ -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 diff --git a/lib/github/ldap/members.rb b/lib/github/ldap/members.rb deleted file mode 100644 index 85b7f37..0000000 --- a/lib/github/ldap/members.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'github/ldap/members/classic' -require 'github/ldap/members/recursive' - -module GitHub - class Ldap - # Provides various strategies for member lookup. - # - # For example: - # - # group = domain.groups(%w(Engineering)).first - # strategy = GitHub::Ldap::Members::Recursive.new(ldap) - # strategy.perform(group) #=> [#] - # - module Members - # Internal: Mapping of strategy name to class. - STRATEGIES = { - :classic => GitHub::Ldap::Members::Classic, - :recursive => GitHub::Ldap::Members::Recursive - } - end - end -end diff --git a/lib/github/ldap/membership_validators/detect.rb b/lib/github/ldap/membership_validators/detect.rb index ba8c4ba..74f7f0d 100644 --- a/lib/github/ldap/membership_validators/detect.rb +++ b/lib/github/ldap/membership_validators/detect.rb @@ -7,9 +7,8 @@ module MembershipValidators # also be defined via `GitHub::Ldap#membership_validator=`. See also # `GitHub::Ldap#configure_membership_validation_strategy`. class Detect < Base - # 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 + # Defines `active_directory_capability?` and necessary helpers. + include GitHub::Ldap::Capabilities def perform(entry) # short circuit validation if there are no groups to check against @@ -48,21 +47,6 @@ def detect_strategy def strategy_config ldap.membership_validator end - - # 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 diff --git a/test/member_search/active_directory_test.rb b/test/member_search/active_directory_test.rb new file mode 100644 index 0000000..e3f367a --- /dev/null +++ b/test/member_search/active_directory_test.rb @@ -0,0 +1,77 @@ +require_relative '../test_helper' + +class GitHubLdapActiveDirectoryMemberSearchStubbedTest < GitHub::Ldap::Test + # Only run when AD integration tests aren't run + def run(*) + self.class.test_env != "activedirectory" ? super : self + end + + def find_group(cn) + @domain.groups([cn]).first + end + + def setup + @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com))) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('user1') + @strategy = GitHub::Ldap::MemberSearch::ActiveDirectory.new(@ldap) + end + + def test_finds_group_members + members = + @ldap.stub :search, [@entry] do + @strategy.perform(find_group("nested-group1")).map(&:dn) + end + assert_includes members, @entry.dn + end + + def test_finds_nested_group_members + members = + @ldap.stub :search, [@entry] do + @strategy.perform(find_group("n-depth-nested-group1")).map(&:dn) + end + assert_includes members, @entry.dn + end + + def test_finds_deeply_nested_group_members + members = + @ldap.stub :search, [@entry] do + @strategy.perform(find_group("n-depth-nested-group9")).map(&:dn) + end + assert_includes members, @entry.dn + end +end + +# See test/support/vm/activedirectory/README.md for details +class GitHubLdapActiveDirectoryMemberSearchIntegrationTest < GitHub::Ldap::Test + # Only run this test suite if ActiveDirectory is configured + def run(*) + self.class.test_env == "activedirectory" ? super : self + end + + def find_group(cn) + @domain.groups([cn]).first + end + + def setup + @ldap = GitHub::Ldap.new(options) + @domain = @ldap.domain(options[:search_domains]) + @entry = @domain.user?('user1') + @strategy = GitHub::Ldap::MemberSearch::ActiveDirectory.new(@ldap) + end + + def test_finds_group_members + members = @strategy.perform(find_group("nested-group1")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_finds_nested_group_members + members = @strategy.perform(find_group("n-depth-nested-group1")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_finds_deeply_nested_group_members + members = @strategy.perform(find_group("n-depth-nested-group9")).map(&:dn) + assert_includes members, @entry.dn + end +end diff --git a/test/members/classic_test.rb b/test/member_search/classic_test.rb similarity index 84% rename from test/members/classic_test.rb rename to test/member_search/classic_test.rb index 6c36a3f..656e12b 100644 --- a/test/members/classic_test.rb +++ b/test/member_search/classic_test.rb @@ -1,11 +1,11 @@ require_relative '../test_helper' -class GitHubLdapRecursiveMembersTest < GitHub::Ldap::Test +class GitHubLdapRecursiveMemberSearchTest < GitHub::Ldap::Test def setup @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com))) @domain = @ldap.domain("dc=github,dc=com") @entry = @domain.user?('user1') - @strategy = GitHub::Ldap::Members::Classic.new(@ldap) + @strategy = GitHub::Ldap::MemberSearch::Classic.new(@ldap) end def find_group(cn) @@ -33,7 +33,7 @@ def test_finds_posix_group_members end def test_does_not_respect_configured_depth_limit - strategy = GitHub::Ldap::Members::Classic.new(@ldap, depth: 2) + strategy = GitHub::Ldap::MemberSearch::Classic.new(@ldap, depth: 2) members = strategy.perform(find_group("n-depth-nested-group9")).map(&:dn) assert_includes members, @entry.dn end diff --git a/test/member_search/detect_test.rb b/test/member_search/detect_test.rb new file mode 100644 index 0000000..6092cdf --- /dev/null +++ b/test/member_search/detect_test.rb @@ -0,0 +1,42 @@ +require_relative '../test_helper' + +# NOTE: Since this strategy is targeted at detecting ActiveDirectory +# capabilities, and we don't have AD setup in CI, we stub out actual queries +# and test against what AD *would* respond with. + +class GitHubLdapDetectMemberSearchTest < GitHub::Ldap::Test + include GitHub::Ldap::Capabilities + + def setup + @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com))) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('user1') + @strategy = GitHub::Ldap::MemberSearch::Detect.new(@ldap) + end + + def test_defers_to_configured_strategy + @ldap.configure_member_search_strategy(:classic) + + assert_kind_of GitHub::Ldap::MemberSearch::Classic, @strategy.strategy + end + + def test_detects_active_directory + caps = Net::LDAP::Entry.new + caps[:supportedcapabilities] = [ACTIVE_DIRECTORY_V61_R2_OID] + + @ldap.stub :capabilities, caps do + assert_kind_of GitHub::Ldap::MemberSearch::ActiveDirectory, + @strategy.strategy + end + end + + def test_falls_back_to_recursive + caps = Net::LDAP::Entry.new + caps[:supportedcapabilities] = [] + + @ldap.stub :capabilities, caps do + assert_kind_of GitHub::Ldap::MemberSearch::Recursive, + @strategy.strategy + end + end +end diff --git a/test/members/recursive_test.rb b/test/member_search/recursive_test.rb similarity index 84% rename from test/members/recursive_test.rb rename to test/member_search/recursive_test.rb index e743ca8..a2d388d 100644 --- a/test/members/recursive_test.rb +++ b/test/member_search/recursive_test.rb @@ -1,11 +1,11 @@ require_relative '../test_helper' -class GitHubLdapRecursiveMembersTest < GitHub::Ldap::Test +class GitHubLdapRecursiveMemberSearchTest < GitHub::Ldap::Test def setup @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com))) @domain = @ldap.domain("dc=github,dc=com") @entry = @domain.user?('user1') - @strategy = GitHub::Ldap::Members::Recursive.new(@ldap) + @strategy = GitHub::Ldap::MemberSearch::Recursive.new(@ldap) end def find_group(cn) @@ -33,7 +33,7 @@ def test_finds_posix_group_members end def test_respects_configured_depth_limit - strategy = GitHub::Ldap::Members::Recursive.new(@ldap, depth: 2) + strategy = GitHub::Ldap::MemberSearch::Recursive.new(@ldap, depth: 2) members = strategy.perform(find_group("n-depth-nested-group9")).map(&:dn) refute_includes members, @entry.dn end diff --git a/test/membership_validators/detect_test.rb b/test/membership_validators/detect_test.rb index 8bf522a..34d476a 100644 --- a/test/membership_validators/detect_test.rb +++ b/test/membership_validators/detect_test.rb @@ -5,6 +5,8 @@ # and test against what AD *would* respond with. class GitHubLdapDetectMembershipValidatorsTest < GitHub::Ldap::Test + include GitHub::Ldap::Capabilities + def setup @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com))) @domain = @ldap.domain("dc=github,dc=com") @@ -26,8 +28,7 @@ def test_defers_to_configured_strategy def test_detects_active_directory caps = Net::LDAP::Entry.new - caps[:supportedcapabilities] = - [GitHub::Ldap::MembershipValidators::Detect::ACTIVE_DIRECTORY_V61_R2_OID] + caps[:supportedcapabilities] = [ACTIVE_DIRECTORY_V61_R2_OID] validator = make_validator(%w(group)) @ldap.stub :capabilities, caps do pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy