diff --git a/lib/github/ldap.rb b/lib/github/ldap.rb index 9844ceb..1484bec 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/membership_validators' include Instrumentation diff --git a/lib/github/ldap/filter.rb b/lib/github/ldap/filter.rb index a238642..64f5aa3 100644 --- a/lib/github/ldap/filter.rb +++ b/lib/github/ldap/filter.rb @@ -20,16 +20,18 @@ def group_filter(group_names) # Filter to check group membership. # - # entry: finds groups this Net::LDAP::Entry is a member of (optional) + # entry: finds groups this entry is a member of (optional) + # Expects a Net::LDAP::Entry or String DN. # # Returns a Net::LDAP::Filter. def member_filter(entry = nil) if entry + entry = entry.dn if entry.respond_to?(:dn) MEMBERSHIP_NAMES. - map {|n| Net::LDAP::Filter.eq(n, entry.dn) }.reduce(:|) + map {|n| Net::LDAP::Filter.eq(n, entry) }.reduce(:|) else MEMBERSHIP_NAMES. - map {|n| Net::LDAP::Filter.pres(n) }. reduce(:|) + map {|n| Net::LDAP::Filter.pres(n) }. reduce(:|) end end @@ -41,10 +43,16 @@ def member_filter(entry = nil) # uid_attr: specifies the memberUid attribute to match with # # Returns a Net::LDAP::Filter or nil if no entry has no UID set. - def posix_member_filter(entry, uid_attr) - if !entry[uid_attr].empty? - entry[uid_attr].map { |uid| Net::LDAP::Filter.eq("memberUid", uid) }. - reduce(:|) + def posix_member_filter(entry_or_uid, uid_attr = nil) + case entry_or_uid + when Net::LDAP::Entry + entry = entry_or_uid + if !entry[uid_attr].empty? + entry[uid_attr].map { |uid| Net::LDAP::Filter.eq("memberUid", uid) }. + reduce(:|) + end + when String + Net::LDAP::Filter.eq("memberUid", entry_or_uid) end end diff --git a/lib/github/ldap/membership_validators.rb b/lib/github/ldap/membership_validators.rb new file mode 100644 index 0000000..9e3b49d --- /dev/null +++ b/lib/github/ldap/membership_validators.rb @@ -0,0 +1,17 @@ +module GitHub + class Ldap + # Provides various strategies for validating membership. + # + # For example: + # + # groups = domain.groups(%w(Engineering)) + # validator = GitHub::Ldap::MembershipValidators::Classic.new(ldap, groups) + # validator.perform(entry) #=> true + # + module MembershipValidators + autoload :Base, 'github/ldap/membership_validators/base' + autoload :Classic, 'github/ldap/membership_validators/classic' + autoload :Recursive, 'github/ldap/membership_validators/recursive' + end + end +end diff --git a/lib/github/ldap/membership_validators/base.rb b/lib/github/ldap/membership_validators/base.rb new file mode 100644 index 0000000..3c47853 --- /dev/null +++ b/lib/github/ldap/membership_validators/base.rb @@ -0,0 +1,37 @@ +module GitHub + class Ldap + module MembershipValidators + class Base + + # Internal: The GitHub::Ldap object to search domains with. + attr_reader :ldap + + # Internal: an Array of Net::LDAP::Entry group objects to validate with. + attr_reader :groups + + # Public: Instantiate new validator. + # + # - ldap: GitHub::Ldap object + # - groups: Array of Net::LDAP::Entry group objects + def initialize(ldap, groups) + @ldap = ldap + @groups = groups + end + + # Abstract: Performs the membership validation check. + # + # Returns Boolean whether the entry's membership is validated or not. + # 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/membership_validators/classic.rb b/lib/github/ldap/membership_validators/classic.rb new file mode 100644 index 0000000..f1cda3e --- /dev/null +++ b/lib/github/ldap/membership_validators/classic.rb @@ -0,0 +1,33 @@ +module GitHub + class Ldap + module MembershipValidators + # Validates membership using `GitHub::Ldap::Domain#membership`. + # + # This is a simple wrapper for existing functionality in order to expose + # it consistently with the new approach. + class Classic < Base + def perform(entry) + return true if groups.empty? + + domains.each do |domain| + membership = domain.membership(entry, group_names) + + if !membership.empty? + entry[:groups] = membership + return true + end + end + + false + end + + # Internal: the group names to look up membership for. + # + # Returns an Array of String group names (CNs). + def group_names + @group_names ||= groups.map { |g| g[:cn].first } + end + end + end + end +end diff --git a/lib/github/ldap/membership_validators/recursive.rb b/lib/github/ldap/membership_validators/recursive.rb new file mode 100644 index 0000000..7d4936c --- /dev/null +++ b/lib/github/ldap/membership_validators/recursive.rb @@ -0,0 +1,90 @@ +module GitHub + class Ldap + module MembershipValidators + # Validates membership recursively. + # + # The first step checks whether the entry is a direct member of the given + # groups. If they are, then we've validated membership successfully. + # + # If not, query for all of the groups that have our groups as members, + # then we check if the entry is a member of any of those. + # + # This is repeated until the entry is found, recursing and requesting + # groups in bulk each iteration until we hit the maximum depth allowed + # and have to give up. + # + # This results in a maximum of `depth` queries (per domain) to validate + # membership in a list of groups. + class Recursive < Base + include Filter + + DEFAULT_MAX_DEPTH = 9 + ATTRS = %w(dn cn) + + def perform(entry, depth = DEFAULT_MAX_DEPTH) + domains.each do |domain| + # find groups entry is an immediate member of + membership = domain.search(filter: member_filter(entry), attributes: ATTRS) + + # success if any of these groups match the restricted auth groups + return true if membership.any? { |entry| group_dns.include?(entry.dn) } + + # give up if the entry has no memberships to recurse + next if membership.empty? + + # recurse to at most `depth` + depth.times do |n| + # find groups whose members include membership groups + membership = domain.search(filter: membership_filter(membership), attributes: ATTRS) + + # success if any of these groups match the restricted auth groups + return true if membership.any? { |entry| group_dns.include?(entry.dn) } + + # give up if there are no more membersips to recurse + break if membership.empty? + end + + # give up on this base if there are no memberships to test + next if membership.empty? + end + + false + end + + # Internal: Construct a filter to find groups this entry is a direct + # member of. + # + # Overloads the included `GitHub::Ldap::Filters#member_filter` method + # to inject `posixGroup` handling. + # + # Returns a Net::LDAP::Filter object. + def member_filter(entry_or_uid, uid = ldap.uid) + filter = super(entry_or_uid) + + if ldap.posix_support_enabled? + if posix_filter = posix_member_filter(entry_or_uid, uid) + filter |= posix_filter + end + end + + filter + end + + # Internal: Construct a filter to find groups whose members are the + # Array of String group DNs passed in. + # + # Returns a String filter. + def membership_filter(groups) + groups.map { |entry| member_filter(entry, :cn) }.reduce(:|) + end + + # Internal: the group DNs to check against. + # + # Returns an Array of String DNs. + def group_dns + @group_dns ||= groups.map(&:dn) + end + end + end + end +end diff --git a/test/filter_test.rb b/test/filter_test.rb index 8fc6ba2..58992a8 100644 --- a/test/filter_test.rb +++ b/test/filter_test.rb @@ -20,7 +20,8 @@ def setup @subject = Subject.new(@ldap) @me = 'uid=calavera,dc=github,dc=com' @uid = "calavera" - @entry = Entry.new(@me, @uid) + @entry = Net::LDAP::Entry.new(@me) + @entry[:uid] = @uid end def test_member_present @@ -32,6 +33,11 @@ def test_member_equal @subject.member_filter(@entry).to_s end + def test_member_equal_with_string + assert_equal "(|(member=#{@me})(uniqueMember=#{@me}))", + @subject.member_filter(@entry.dn).to_s + end + def test_posix_member_without_uid @entry.uid = nil assert_nil @subject.posix_member_filter(@entry, @ldap.uid) @@ -42,6 +48,11 @@ def test_posix_member_equal @subject.posix_member_filter(@entry, @ldap.uid).to_s end + def test_posix_member_equal_string + assert_equal "(memberUid=#{@uid})", + @subject.posix_member_filter(@uid).to_s + end + def test_groups_reduced assert_equal "(|(cn=Enterprise)(cn=People))", @subject.group_filter(%w(Enterprise People)).to_s diff --git a/test/fixtures/github-with-posixGroups.ldif b/test/fixtures/github-with-posixGroups.ldif index ac8b3a0..5269784 100644 --- a/test/fixtures/github-with-posixGroups.ldif +++ b/test/fixtures/github-with-posixGroups.ldif @@ -27,6 +27,29 @@ objectClass: posixGroup memberUid: benburkert memberUid: mtodd +dn: cn=group1,ou=groups,dc=github,dc=com +cn: group1 +objectClass: posixGroup +memberUid: group1.1 +memberUid: user1 + +dn: cn=group1.1,ou=groups,dc=github,dc=com +cn: group1.1 +objectClass: posixGroup +memberUid: group1.1.1 +memberUid: user1.1 + +dn: cn=group1.1.1,ou=groups,dc=github,dc=com +cn: group1.1.1 +objectClass: posixGroup +memberUid: group1.1.1.1 +memberUid: user1.1.1 + +dn: cn=group1.1.1.1,ou=groups,dc=github,dc=com +cn: group1.1.1.1 +objectClass: posixGroup +memberUid: user1.1.1.1 + # Users dn: ou=users,dc=github,dc=com @@ -48,3 +71,35 @@ uid: mtodd userPassword: passworD1 mail: mtodd@github.com objectClass: inetOrgPerson + +dn: uid=user1,ou=users,dc=github,dc=com +cn: user1 +sn: user1 +uid: user1 +userPassword: passworD1 +mail: user1@github.com +objectClass: inetOrgPerson + +dn: uid=user1.1,ou=users,dc=github,dc=com +cn: user1.1 +sn: user1.1 +uid: user1.1 +userPassword: passworD1 +mail: user1.1@github.com +objectClass: inetOrgPerson + +dn: uid=user1.1.1,ou=users,dc=github,dc=com +cn: user1.1.1 +sn: user1.1.1 +uid: user1.1.1 +userPassword: passworD1 +mail: user1.1.1@github.com +objectClass: inetOrgPerson + +dn: uid=user1.1.1.1,ou=users,dc=github,dc=com +cn: user1.1.1.1 +sn: user1.1.1.1 +uid: user1.1.1.1 +userPassword: passworD1 +mail: user1.1.1.1@github.com +objectClass: inetOrgPerson diff --git a/test/fixtures/github-with-subgroups.ldif b/test/fixtures/github-with-subgroups.ldif index 00dc929..c432edc 100644 --- a/test/fixtures/github-with-subgroups.ldif +++ b/test/fixtures/github-with-subgroups.ldif @@ -50,19 +50,19 @@ member: uid=user1,ou=users,dc=github,dc=com member: cn=group1.1,ou=groups,dc=github,dc=com dn: cn=group1.1,ou=groups,dc=github,dc=com -cn: group1 +cn: group1.1 objectClass: groupOfNames member: uid=user1.1,ou=users,dc=github,dc=com member: cn=group1.1.1,ou=groups,dc=github,dc=com dn: cn=group1.1.1,ou=groups,dc=github,dc=com -cn: group1 +cn: group1.1.1 objectClass: groupOfNames member: uid=user1.1.1,ou=users,dc=github,dc=com member: cn=group1.1.1.1,ou=groups,dc=github,dc=com dn: cn=group1.1.1.1,ou=groups,dc=github,dc=com -cn: group1 +cn: group1.1.1.1 objectClass: groupOfNames member: uid=user1.1.1.1,ou=users,dc=github,dc=com diff --git a/test/membership_validators/classic_test.rb b/test/membership_validators/classic_test.rb new file mode 100644 index 0000000..7802826 --- /dev/null +++ b/test/membership_validators/classic_test.rb @@ -0,0 +1,79 @@ +require_relative '../test_helper' + +class GitHubLdapClassicMembershipValidatorsTest < GitHub::Ldap::Test + def self.test_server_options + { search_domains: "dc=github,dc=com", + user_fixtures: FIXTURES.join('github-with-subgroups.ldif').to_s + } + end + + def setup + @ldap = GitHub::Ldap.new(options) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('user1.1.1.1') + @validator = GitHub::Ldap::MembershipValidators::Classic + end + + def make_validator(groups) + groups = @domain.groups(groups) + @validator.new(@ldap, groups) + end + + def test_validates_user_in_group + validator = make_validator(%w(group1.1.1.1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_child_group + validator = make_validator(%w(group1.1.1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_grandchild_group + validator = make_validator(%w(group1.1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_great_grandchild_group + validator = make_validator(%w(group1)) + assert validator.perform(@entry) + end + + def test_does_not_validate_user_not_in_group + validator = make_validator(%w(Enterprise)) + refute validator.perform(@entry) + end + + def test_does_not_validate_user_not_in_any_group + @entry = @domain.user?('admin') + validator = make_validator(%w(Enterprise)) + refute validator.perform(@entry) + end +end + +class GitHubLdapClassicMembershipValidatorsWithPosixGroupsTest < GitHub::Ldap::Test + def self.test_server_options + { search_domains: "dc=github,dc=com", + uid: "uid", + custom_schemas: FIXTURES.join('posixGroup.schema.ldif'), + user_fixtures: FIXTURES.join('github-with-posixGroups.ldif').to_s + } + end + + def setup + @ldap = GitHub::Ldap.new(options) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('user1.1.1.1') + @validator = GitHub::Ldap::MembershipValidators::Classic + end + + def make_validator(groups) + groups = @domain.groups(groups) + @validator.new(@ldap, groups) + end + + def test_validates_user_in_group + validator = make_validator(%w(group1.1.1.1)) + assert validator.perform(@entry) + end +end diff --git a/test/membership_validators/recursive_test.rb b/test/membership_validators/recursive_test.rb new file mode 100644 index 0000000..fd8e7ee --- /dev/null +++ b/test/membership_validators/recursive_test.rb @@ -0,0 +1,105 @@ +require_relative '../test_helper' + +class GitHubLdapRecursiveMembershipValidatorsTest < GitHub::Ldap::Test + def self.test_server_options + { search_domains: "dc=github,dc=com", + uid: "uid", + user_fixtures: FIXTURES.join('github-with-subgroups.ldif').to_s + } + end + + def setup + @ldap = GitHub::Ldap.new(options) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('user1.1.1.1') + @validator = GitHub::Ldap::MembershipValidators::Recursive + end + + def make_validator(groups) + groups = @domain.groups(groups) + @validator.new(@ldap, groups) + end + + def test_validates_user_in_group + validator = make_validator(%w(group1.1.1.1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_child_group + validator = make_validator(%w(group1.1.1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_grandchild_group + validator = make_validator(%w(group1.1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_great_grandchild_group + validator = make_validator(%w(group1)) + assert validator.perform(@entry) + end + + def test_does_not_validate_user_in_great_granchild_group_with_depth + validator = make_validator(%w(group1)) + refute validator.perform(@entry, 2) + end + + def test_does_not_validate_user_not_in_group + validator = make_validator(%w(Enterprise)) + refute validator.perform(@entry) + end + + def test_does_not_validate_user_not_in_any_group + @entry = @domain.user?('admin') + validator = make_validator(%w(Enterprise)) + refute validator.perform(@entry) + end +end + +class GitHubLdapRecursiveMembershipValidatorsWithPosixGroupsTest < GitHub::Ldap::Test + def self.test_server_options + { search_domains: "dc=github,dc=com", + uid: "uid", + custom_schemas: FIXTURES.join('posixGroup.schema.ldif'), + user_fixtures: FIXTURES.join('github-with-posixGroups.ldif').to_s + } + end + + def setup + @ldap = GitHub::Ldap.new(options) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('user1.1.1.1') + @validator = GitHub::Ldap::MembershipValidators::Recursive + end + + def make_validator(groups) + groups = @domain.groups(groups) + @validator.new(@ldap, groups) + end + + def test_validates_user_in_group + validator = make_validator(%w(group1.1.1.1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_child_group + validator = make_validator(%w(group1.1.1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_grandchild_group + validator = make_validator(%w(group1.1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_great_grandchild_group + validator = make_validator(%w(group1)) + assert validator.perform(@entry) + end + + def test_does_not_validate_user_in_great_granchild_group_with_depth + validator = make_validator(%w(group1)) + refute validator.perform(@entry, 2) + end +end diff --git a/test/membership_validators_test.rb b/test/membership_validators_test.rb new file mode 100644 index 0000000..77f0358 --- /dev/null +++ b/test/membership_validators_test.rb @@ -0,0 +1,58 @@ +require_relative 'test_helper' + +module GitHubLdapMembershipValidatorsTestCases + def make_validator(groups) + groups = @domain.groups(groups) + @validator.new(@ldap, groups) + end + + def test_validates_user_in_group + validator = make_validator(%w(Enterprise)) + assert validator.perform(@entry) + end + + def test_does_not_validate_user_not_in_group + validator = make_validator(%w(People)) + refute validator.perform(@entry) + end + + def test_does_not_validate_user_not_in_any_group + @entry = @domain.user?('ldaptest') + validator = make_validator(%w(Enterprise People)) + refute validator.perform(@entry) + end +end + +class GitHubLdapMembershipValidatorsClassicTest < GitHub::Ldap::Test + include GitHubLdapMembershipValidatorsTestCases + + def self.test_server_options + { search_domains: "dc=github,dc=com", + uid: "uid" + } + end + + def setup + @ldap = GitHub::Ldap.new(options) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('calavera') + @validator = GitHub::Ldap::MembershipValidators::Classic + end +end + +class GitHubLdapMembershipValidatorsRecursiveTest < GitHub::Ldap::Test + include GitHubLdapMembershipValidatorsTestCases + + def self.test_server_options + { search_domains: "dc=github,dc=com", + uid: "uid" + } + end + + def setup + @ldap = GitHub::Ldap.new(options) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('calavera') + @validator = GitHub::Ldap::MembershipValidators::Recursive + 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