diff --git a/lib/github/ldap.rb b/lib/github/ldap.rb index 3258ac4..43c3f3b 100644 --- a/lib/github/ldap.rb +++ b/lib/github/ldap.rb @@ -9,6 +9,7 @@ class Ldap require 'github/ldap/virtual_group' require 'github/ldap/virtual_attributes' require 'github/ldap/instrumentation' + require 'github/ldap/members' require 'github/ldap/membership_validators' include Instrumentation diff --git a/lib/github/ldap/domain.rb b/lib/github/ldap/domain.rb index aa2066b..8fd904f 100644 --- a/lib/github/ldap/domain.rb +++ b/lib/github/ldap/domain.rb @@ -163,8 +163,11 @@ def search(options, &block) # Get the entry for this domain. # # Returns a Net::LDAP::Entry - def bind - search(size: 1, scope: Net::LDAP::SearchScope_BaseObject).first + def bind(options = {}) + options[:size] = 1 + options[:scope] = Net::LDAP::SearchScope_BaseObject + options[:attributes] ||= [] + search(options).first end end end diff --git a/lib/github/ldap/members.rb b/lib/github/ldap/members.rb new file mode 100644 index 0000000..85b7f37 --- /dev/null +++ b/lib/github/ldap/members.rb @@ -0,0 +1,22 @@ +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/members/classic.rb b/lib/github/ldap/members/classic.rb new file mode 100644 index 0000000..81a5601 --- /dev/null +++ b/lib/github/ldap/members/classic.rb @@ -0,0 +1,30 @@ +module GitHub + class Ldap + module Members + # 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 + + # Public: Performs search for group members, including groups and + # members of subgroups recursively. + # + # Returns Array of Net::LDAP::Entry objects. + def perform(group_entry) + group = ldap.load_group(group_entry) + group.members + group.subgroups + end + end + end + end +end diff --git a/lib/github/ldap/members/recursive.rb b/lib/github/ldap/members/recursive.rb new file mode 100644 index 0000000..0e1bef9 --- /dev/null +++ b/lib/github/ldap/members/recursive.rb @@ -0,0 +1,139 @@ +module GitHub + class Ldap + module Members + # 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 + include Filter + + DEFAULT_MAX_DEPTH = 9 + ATTRS = %w(dn 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 + + # Public: Instantiate new search strategy. + # + # - ldap: GitHub::Ldap object + # - options: Hash of options + def initialize(ldap, options = {}) + @ldap = ldap + @options = options + @depth = options[:depth] || DEFAULT_MAX_DEPTH + end + + # Public: Performs search for group members, including groups and + # members of subgroups recursively. + # + # Returns Array of Net::LDAP::Entry objects. + def perform(group) + found = Hash.new + + # find members (N queries) + entries = member_entries(group) + return [] if entries.empty? + + # track found entries + entries.each do |entry| + found[entry.dn] = entry + end + + # descend to `depth` levels, at most + depth.times do |n| + # find every (new, unique) member entry + depth_subentries = entries.each_with_object([]) do |entry, depth_entries| + submembers = entry["member"] + + # skip any members we've already found + submembers.reject! { |dn| found.key?(dn) } + + # find members of subgroup, including subgroups (N queries) + subentries = member_entries(entry) + next if subentries.empty? + + # track found subentries + subentries.each { |entry| found[entry.dn] = entry } + + # collect all entries for this depth + depth_entries.concat subentries + end + + # stop if there are no more subgroups to search + break if depth_subentries.empty? + + # go one level deeper + entries = depth_subentries + end + + # return all found entries + found.values + end + + # Internal: Fetch member entries, including subgroups, for the given + # entry. + # + # Returns an Array of Net::LDAP::Entry objects. + def member_entries(entry) + entries = [] + dns = member_dns(entry) + uids = member_uids(entry) + + entries.concat entries_by_uid(uids) unless uids.empty? + entries.concat entries_by_dn(dns) unless dns.empty? + + entries + end + private :member_entries + + # Internal: Bind a list of DNs to their respective entries. + # + # Returns an Array of Net::LDAP::Entry objects. + def entries_by_dn(members) + members.map do |dn| + ldap.domain(dn).bind(attributes: ATTRS) + end.compact + end + private :entries_by_dn + + # Internal: Fetch entries by UID. + # + # Returns an Array of Net::LDAP::Entry objects. + def entries_by_uid(members) + filter = members.map { |uid| Net::LDAP::Filter.eq(ldap.uid, uid) }.reduce(:|) + domains.each_with_object([]) do |domain, entries| + entries.concat domain.search(filter: filter, attributes: ATTRS) + end.compact + end + private :entries_by_uid + + # Internal: Returns an Array of String DNs for `groupOfNames` and + # `uniqueGroupOfNames` members. + def member_dns(entry) + MEMBERSHIP_NAMES.each_with_object([]) do |attr_name, members| + members.concat entry[attr_name] + end + end + private :member_dns + + # Internal: Returns an Array of String UIDs for PosixGroups members. + 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 +end diff --git a/test/members/classic_test.rb b/test/members/classic_test.rb new file mode 100644 index 0000000..6c36a3f --- /dev/null +++ b/test/members/classic_test.rb @@ -0,0 +1,40 @@ +require_relative '../test_helper' + +class GitHubLdapRecursiveMembersTest < 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) + end + + def find_group(cn) + @domain.groups([cn]).first + 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 + + def test_finds_posix_group_members + members = @strategy.perform(find_group("posix-group1")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_does_not_respect_configured_depth_limit + strategy = GitHub::Ldap::Members::Classic.new(@ldap, depth: 2) + members = strategy.perform(find_group("n-depth-nested-group9")).map(&:dn) + assert_includes members, @entry.dn + end +end diff --git a/test/members/recursive_test.rb b/test/members/recursive_test.rb new file mode 100644 index 0000000..e743ca8 --- /dev/null +++ b/test/members/recursive_test.rb @@ -0,0 +1,40 @@ +require_relative '../test_helper' + +class GitHubLdapRecursiveMembersTest < 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) + end + + def find_group(cn) + @domain.groups([cn]).first + 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 + + def test_finds_posix_group_members + members = @strategy.perform(find_group("posix-group1")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_respects_configured_depth_limit + strategy = GitHub::Ldap::Members::Recursive.new(@ldap, depth: 2) + members = strategy.perform(find_group("n-depth-nested-group9")).map(&:dn) + refute_includes members, @entry.dn + end +end 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