Skip to content

Commit fc12824

Browse files
committed
Add first pass at VirtualAttributes membership validator
1 parent 0bdbdf2 commit fc12824

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed

lib/github/ldap/membership_validators.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'github/ldap/membership_validators/base'
22
require 'github/ldap/membership_validators/classic'
33
require 'github/ldap/membership_validators/recursive'
4+
require 'github/ldap/membership_validators/virtual_attributes'
45

56
module GitHub
67
class Ldap
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
module GitHub
2+
class Ldap
3+
module MembershipValidators
4+
# Validates membership recursively using virtual attributes.
5+
class VirtualAttributes < Base
6+
include Filter
7+
8+
DEFAULT_MAX_DEPTH = 9
9+
ATTRS = %w(dn cn memberOf)
10+
11+
def perform(entry, depth = DEFAULT_MAX_DEPTH)
12+
return true if groups.empty?
13+
14+
domains.each do |domain|
15+
# get groups entry is an immediate member of
16+
membership = entry[member_of_attr]
17+
18+
# success if any of these groups match the restricted auth groups
19+
return true if membership.any? { |entry| group_dns.include?(entry.dn) }
20+
21+
# give up if the entry has no memberships to recurse
22+
next if membership.empty?
23+
24+
# recurse to at most `depth`
25+
depth.times do |n|
26+
# find groups whose members include membership groups
27+
membership = domain.search(filter: membership_filter(membership), attributes: ATTRS)
28+
29+
# success if any of these groups match the restricted auth groups
30+
return true if membership.any? { |entry| group_dns.include?(entry.dn) }
31+
32+
# give up if there are no more membersips to recurse
33+
break if membership.empty?
34+
end
35+
36+
# give up on this base if there are no memberships to test
37+
next if membership.empty?
38+
end
39+
40+
false
41+
end
42+
43+
# Internal: Returns the String memberOf virtual attribute name.
44+
def member_of_attr
45+
@member_of_attr ||= ldap.virtual_attributes.virtual_membership
46+
end
47+
48+
# Internal: Construct a filter to find groups this entry is a direct
49+
# member of.
50+
#
51+
# Overloads the included `GitHub::Ldap::Filters#member_filter` method
52+
# to inject `posixGroup` handling.
53+
#
54+
# Returns a Net::LDAP::Filter object.
55+
def member_filter(entry)
56+
Net::LDAP::Filter.eq(member_of_attr, entry.dn)
57+
end
58+
59+
# Internal: Construct a filter to find groups whose members are the
60+
# Array of String group DNs passed in.
61+
#
62+
# Returns a String filter.
63+
def membership_filter(groups)
64+
groups.map { |entry| member_filter(entry) }.reduce(:|)
65+
end
66+
67+
# Internal: the group DNs to check against.
68+
#
69+
# Returns an Array of String DNs.
70+
def group_dns
71+
@group_dns ||= groups.map(&:dn)
72+
end
73+
end
74+
end
75+
end
76+
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
require_relative '../test_helper'
2+
3+
class GitHubLdapVirtualAttributesMembershipValidatorsTest < GitHub::Ldap::Test
4+
def setup
5+
opts = options.merge \
6+
search_domains: %w(dc=github,dc=com),
7+
virtual_attributes: true
8+
@ldap = GitHub::Ldap.new(opts)
9+
@domain = @ldap.domain("dc=github,dc=com")
10+
@entry = @domain.user?('user1', :attributes => %w(dn cn memberOf))
11+
@validator = GitHub::Ldap::MembershipValidators::VirtualAttributes
12+
end
13+
14+
def make_validator(groups)
15+
groups = @domain.groups(groups)
16+
@validator.new(@ldap, groups)
17+
end
18+
19+
def test_validates_user_in_group
20+
validator = make_validator(%w(nested-group1))
21+
assert validator.perform(@entry)
22+
end
23+
24+
def test_validates_user_in_child_group
25+
validator = make_validator(%w(n-depth-nested-group1))
26+
assert validator.perform(@entry)
27+
end
28+
29+
def test_validates_user_in_grandchild_group
30+
validator = make_validator(%w(n-depth-nested-group2))
31+
assert validator.perform(@entry)
32+
end
33+
34+
def test_validates_user_in_great_grandchild_group
35+
validator = make_validator(%w(n-depth-nested-group3))
36+
assert validator.perform(@entry)
37+
end
38+
39+
def test_does_not_validate_user_in_great_granchild_group_with_depth
40+
validator = make_validator(%w(n-depth-nested-group3))
41+
refute validator.perform(@entry, 2)
42+
end
43+
44+
def test_does_not_validate_user_not_in_group
45+
validator = make_validator(%w(ghe-admins))
46+
refute validator.perform(@entry)
47+
end
48+
49+
def test_does_not_validate_user_not_in_any_group
50+
@entry = @domain.user?('groupless-user1')
51+
validator = make_validator(%w(all-users))
52+
refute validator.perform(@entry)
53+
end
54+
55+
def test_validates_user_in_posix_group
56+
validator = make_validator(%w(posix-group1))
57+
assert validator.perform(@entry)
58+
end
59+
end

0 commit comments

Comments
 (0)