github-ldap 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +15 -2
  3. data/CHANGELOG.md +13 -0
  4. data/Gemfile +4 -0
  5. data/README.md +15 -1
  6. data/Rakefile +1 -1
  7. data/github-ldap.gemspec +2 -2
  8. data/lib/github/ldap.rb +55 -12
  9. data/lib/github/ldap/domain.rb +6 -2
  10. data/lib/github/ldap/filter.rb +15 -7
  11. data/lib/github/ldap/group.rb +1 -1
  12. data/lib/github/ldap/instrumentation.rb +28 -0
  13. data/lib/github/ldap/membership_validators.rb +18 -0
  14. data/lib/github/ldap/membership_validators/active_directory.rb +56 -0
  15. data/lib/github/ldap/membership_validators/base.rb +37 -0
  16. data/lib/github/ldap/membership_validators/classic.rb +34 -0
  17. data/lib/github/ldap/membership_validators/recursive.rb +93 -0
  18. data/lib/github/ldap/server.rb +2 -0
  19. data/script/changelog +29 -0
  20. data/script/cibuild-apacheds +7 -0
  21. data/script/cibuild-openldap +7 -0
  22. data/script/install-openldap +44 -0
  23. data/script/package +7 -0
  24. data/script/release +16 -0
  25. data/test/domain_test.rb +71 -89
  26. data/test/filter_test.rb +12 -1
  27. data/test/fixtures/common/seed.ldif +369 -0
  28. data/test/fixtures/openldap/memberof.ldif +33 -0
  29. data/test/fixtures/openldap/slapd.conf.ldif +67 -0
  30. data/test/fixtures/posixGroup.schema.ldif +34 -8
  31. data/test/group_test.rb +19 -25
  32. data/test/ldap_test.rb +28 -21
  33. data/test/membership_validators/active_directory_test.rb +68 -0
  34. data/test/membership_validators/classic_test.rb +51 -0
  35. data/test/membership_validators/recursive_test.rb +56 -0
  36. data/test/membership_validators_test.rb +46 -0
  37. data/test/posix_group_test.rb +25 -28
  38. data/test/support/vm/openldap/.gitignore +1 -0
  39. data/test/support/vm/openldap/README.md +32 -0
  40. data/test/support/vm/openldap/Vagrantfile +35 -0
  41. data/test/test_helper.rb +72 -10
  42. metadata +52 -27
  43. data/test/fixtures/github-with-looped-subgroups.ldif +0 -82
  44. data/test/fixtures/github-with-missing-entries.ldif +0 -85
  45. data/test/fixtures/github-with-posixGroups.ldif +0 -50
  46. data/test/fixtures/github-with-subgroups.ldif +0 -146
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f417737b5a49cba30ca466e8ef2a361894d4069a
4
- data.tar.gz: fad20f08e322deb3b0a3303f4a5b9b2c0fce1469
3
+ metadata.gz: 053a59e45e7ee63a06ee943da318bf9362455306
4
+ data.tar.gz: d2b435150ad904b336490fa9e5bc863b6ab6d38d
5
5
  SHA512:
6
- metadata.gz: 7fed87fe08383e0a6a264e99396aa8a7e9ed22d85dd5bdba0f100f9b558c5ac548718d1e7825cc01022329a877f0471138bab7eca2679d5f6c602ba9fb7bba1b
7
- data.tar.gz: ee4bf4726436769b4109d052b2d5071e1f1ef26029ee3ab5b29ee020d60dcfdfff867ef11a8b4f624ec271ac60f46f153007f6dbe8220825e1aff8f380327a42
6
+ metadata.gz: 195ae9040327fb236460618d6efeaf5588f07d33edc06220d312790643d81b6892ec063045dac7e4de5dd73444e8cead35baf1555ed57659855d519d0e297a15
7
+ data.tar.gz: a5a047f799e6653cfbfc83749ed452ea2e3b0af1452930cdae78b51bf94461ce0bbb23ebe5a2b1672f348a1826a21633552680ae16cff56ddfffcc0a249e1b07
data/.travis.yml CHANGED
@@ -1,7 +1,20 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
- - 2.1.0
3
+ - 1.9.3
4
+ - 2.1.0
5
5
 
6
+ env:
7
+ - TESTENV=openldap
8
+ - TESTENV=apacheds
9
+
10
+ install:
11
+ - if [ "$TESTENV" = "openldap" ]; then ./script/install-openldap; fi
12
+ - bundle install
13
+
14
+ script:
15
+ - ./script/cibuild-$TESTENV
16
+
17
+ matrix:
18
+ fast_finish: true
6
19
  notifications:
7
20
  email: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # CHANGELOG
2
+
3
+ ## v1.4.0
4
+
5
+ * Document constructor options [#57](https://github.com/github/github-ldap/pull/57)
6
+ * [CI] Add Vagrant box for running tests against OpenLDAP locally [#55](https://github.com/github/github-ldap/pull/55)
7
+ * Run all tests, including those in subdirectories [#54](https://github.com/github/github-ldap/pull/54)
8
+ * Add ActiveDirectory membership validator [#52](https://github.com/github/github-ldap/pull/52)
9
+ * Merge dev-v2 branch into master [#50](https://github.com/github/github-ldap/pull/50)
10
+ * Pass through search options for GitHub::Ldap::Domain#user? [#51](https://github.com/github/github-ldap/pull/51)
11
+ * Fix membership validation tests [#49](https://github.com/github/github-ldap/pull/49)
12
+ * Add CI build for OpenLDAP integration [#48](https://github.com/github/github-ldap/pull/48)
13
+ * Membership Validators [#45](https://github.com/github/github-ldap/pull/45)
data/Gemfile CHANGED
@@ -2,3 +2,7 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in github-ldap.gemspec
4
4
  gemspec
5
+
6
+ group :test, :development do
7
+ gem "byebug", :platforms => [:mri_20, :mri_21]
8
+ end
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <a href="https://travis-ci.org/github/github-ldap">![Build Status](https://travis-ci.org/github/github-ldap.png)</a>
1
+ <a href="https://travis-ci.org/github/github-ldap">![Build Status](https://travis-ci.org/github/github-ldap.png?branch=master)</a>
2
2
 
3
3
  # Github::Ldap
4
4
 
@@ -42,6 +42,8 @@ Initialize a new adapter using those required options:
42
42
  ldap = GitHub::Ldap.new options
43
43
  ```
44
44
 
45
+ See GitHub::Ldap#initialize for additional options.
46
+
45
47
  ### Querying
46
48
 
47
49
  Searches are performed against an individual domain base, so the first step is to get a new `GitHub::Ldap::Domain` object for the connection:
@@ -128,3 +130,15 @@ end
128
130
  3. Commit your changes (`git commit -am 'Add some feature'`)
129
131
  4. Push to the branch (`git push origin my-new-feature`)
130
132
  5. Create new Pull Request
133
+
134
+ ## Releasing
135
+
136
+ This section is for gem maintainers to cut a new version of the gem. See
137
+ [jch/release-scripts](https://github.com/jch/release-scripts) for original
138
+ source of release scripts.
139
+
140
+ * Create a new branch from `master` named `release-x.y.z`, where `x.y.z` is the version to be released
141
+ * Update `github-ldap.gemspec` to x.y.z following [semver](http://semver.org)
142
+ * Run `script/changelog` and paste the draft into `CHANGELOG.md`. Edit as needed
143
+ * Create pull request to solict feedback
144
+ * After merging the pull request, on the master branch, run `script/release`
data/Rakefile CHANGED
@@ -3,7 +3,7 @@ require 'rake/testtask'
3
3
 
4
4
  Rake::TestTask.new do |t|
5
5
  t.libs << "test"
6
- t.pattern = "test/*_test.rb"
6
+ t.pattern = "test/**/*_test.rb"
7
7
  end
8
8
 
9
9
  task :default => :test
data/github-ldap.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "github-ldap"
5
- spec.version = "1.3.3"
5
+ spec.version = "1.4.0"
6
6
  spec.authors = ["David Calavera"]
7
7
  spec.email = ["david.calavera@gmail.com"]
8
8
  spec.description = %q{Ldap authentication for humans}
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
16
  spec.require_paths = ["lib"]
17
17
 
18
- spec.add_dependency 'net-ldap', '~> 0.7.0'
18
+ spec.add_dependency 'net-ldap', '~> 0.9.0'
19
19
 
20
20
  spec.add_development_dependency "bundler", "~> 1.3"
21
21
  spec.add_development_dependency 'ladle'
data/lib/github/ldap.rb CHANGED
@@ -8,6 +8,10 @@ module GitHub
8
8
  require 'github/ldap/posix_group'
9
9
  require 'github/ldap/virtual_group'
10
10
  require 'github/ldap/virtual_attributes'
11
+ require 'github/ldap/instrumentation'
12
+ require 'github/ldap/membership_validators'
13
+
14
+ include Instrumentation
11
15
 
12
16
  extend Forwardable
13
17
 
@@ -23,12 +27,45 @@ module GitHub
23
27
  # Returns a Net::LDAP::Entry if the operation succeeded.
24
28
  def_delegator :@connection, :bind
25
29
 
26
- attr_reader :uid, :search_domains, :virtual_attributes
30
+ # Public - Opens a connection to the server and keeps it open for the
31
+ # duration of the block.
32
+ #
33
+ # Returns the return value of the block.
34
+ def_delegator :@connection, :open
35
+
36
+ attr_reader :uid, :search_domains, :virtual_attributes,
37
+ :instrumentation_service
27
38
 
39
+ # Build a new GitHub::Ldap instance
40
+ #
41
+ # ## Connection
42
+ #
43
+ # host: required string ldap server host address
44
+ # port: required string or number ldap server port
45
+ # encryption: optional string. `ssl` or `tls`. nil by default
46
+ # admin_user: optional string ldap administrator user dn for authentication
47
+ # admin_password: optional string ldap administrator user password
48
+ #
49
+ # ## Behavior
50
+ #
51
+ # uid: optional field name used to authenticate users. Defaults to `sAMAccountName` (what ActiveDirectory uses)
52
+ # virtual_attributes: optional. boolean true to use server's virtual attributes. Hash to specify custom mapping. Default false.
53
+ # recursive_group_search_fallback: optional boolean whether membership checks should recurse into nested groups when virtual attributes aren't enabled. Default false.
54
+ # posix_support: optional boolean `posixGroup` support. Default true.
55
+ # search_domains: optional array of string bases to search through
56
+ #
57
+ # ## Diagnostics
58
+ #
59
+ # instrumentation_service: optional ActiveSupport::Notifications compatible object
60
+ #
28
61
  def initialize(options = {})
29
62
  @uid = options[:uid] || "sAMAccountName"
30
63
 
31
- @connection = Net::LDAP.new({host: options[:host], port: options[:port]})
64
+ @connection = Net::LDAP.new({
65
+ host: options[:host],
66
+ port: options[:port],
67
+ instrumentation_service: options[:instrumentation_service]
68
+ })
32
69
 
33
70
  if options[:admin_user] && options[:admin_password]
34
71
  @connection.authenticate(options[:admin_user], options[:admin_password])
@@ -49,6 +86,9 @@ module GitHub
49
86
  # search_domains is a connection of bases to perform searches
50
87
  # when a base is not explicitly provided.
51
88
  @search_domains = Array(options[:search_domains])
89
+
90
+ # enables instrumenting queries
91
+ @instrumentation_service = options[:instrumentation_service]
52
92
  end
53
93
 
54
94
  # Public - Whether membership checks should recurse into nested groups when
@@ -126,17 +166,20 @@ module GitHub
126
166
  #
127
167
  # Returns an Array of Net::LDAP::Entry.
128
168
  def search(options, &block)
129
- result = if options[:base]
130
- @connection.search(options, &block)
131
- else
132
- search_domains.each_with_object([]) do |base, result|
133
- rs = @connection.search(options.merge(:base => base), &block)
134
- result.concat Array(rs) unless rs == false
135
- end
169
+ instrument "search.github_ldap", options.dup do |payload|
170
+ result =
171
+ if options[:base]
172
+ @connection.search(options, &block)
173
+ else
174
+ search_domains.each_with_object([]) do |base, result|
175
+ rs = @connection.search(options.merge(:base => base), &block)
176
+ result.concat Array(rs) unless rs == false
177
+ end
178
+ end
179
+
180
+ return [] if result == false
181
+ Array(result)
136
182
  end
137
-
138
- return [] if result == false
139
- Array(result)
140
183
  end
141
184
 
142
185
  # Internal - Determine whether to use encryption or not.
@@ -110,11 +110,15 @@ module GitHub
110
110
  # Check if a user exists based in the `uid`.
111
111
  #
112
112
  # login: is the user's login
113
+ # search_options: Net::LDAP#search compatible options to pass through
113
114
  #
114
115
  # Returns the user if the login matches any `uid`.
115
116
  # Returns nil if there are no matches.
116
- def user?(login)
117
- search(filter: login_filter(@uid, login), size: 1).first
117
+ def user?(login, search_options = {})
118
+ options = search_options.merge \
119
+ filter: login_filter(@uid, login),
120
+ size: 1
121
+ search(options).first
118
122
  end
119
123
 
120
124
  # Check if a user can be bound with a password.
@@ -20,16 +20,18 @@ module GitHub
20
20
 
21
21
  # Filter to check group membership.
22
22
  #
23
- # entry: finds groups this Net::LDAP::Entry is a member of (optional)
23
+ # entry: finds groups this entry is a member of (optional)
24
+ # Expects a Net::LDAP::Entry or String DN.
24
25
  #
25
26
  # Returns a Net::LDAP::Filter.
26
27
  def member_filter(entry = nil)
27
28
  if entry
29
+ entry = entry.dn if entry.respond_to?(:dn)
28
30
  MEMBERSHIP_NAMES.
29
- map {|n| Net::LDAP::Filter.eq(n, entry.dn) }.reduce(:|)
31
+ map {|n| Net::LDAP::Filter.eq(n, entry) }.reduce(:|)
30
32
  else
31
33
  MEMBERSHIP_NAMES.
32
- map {|n| Net::LDAP::Filter.pres(n) }. reduce(:|)
34
+ map {|n| Net::LDAP::Filter.pres(n) }. reduce(:|)
33
35
  end
34
36
  end
35
37
 
@@ -41,10 +43,16 @@ module GitHub
41
43
  # uid_attr: specifies the memberUid attribute to match with
42
44
  #
43
45
  # Returns a Net::LDAP::Filter or nil if no entry has no UID set.
44
- def posix_member_filter(entry, uid_attr)
45
- if !entry[uid_attr].empty?
46
- entry[uid_attr].map { |uid| Net::LDAP::Filter.eq("memberUid", uid) }.
47
- reduce(:|)
46
+ def posix_member_filter(entry_or_uid, uid_attr = nil)
47
+ case entry_or_uid
48
+ when Net::LDAP::Entry
49
+ entry = entry_or_uid
50
+ if !entry[uid_attr].empty?
51
+ entry[uid_attr].map { |uid| Net::LDAP::Filter.eq("memberUid", uid) }.
52
+ reduce(:|)
53
+ end
54
+ when String
55
+ Net::LDAP::Filter.eq("memberUid", entry_or_uid)
48
56
  end
49
57
  end
50
58
 
@@ -12,7 +12,7 @@ module GitHub
12
12
  class Group
13
13
  include Filter
14
14
 
15
- GROUP_CLASS_NAMES = %w(groupOfNames groupOfUniqueNames posixGroup)
15
+ GROUP_CLASS_NAMES = %w(groupOfNames groupOfUniqueNames posixGroup group)
16
16
 
17
17
  attr_reader :ldap, :entry
18
18
 
@@ -0,0 +1,28 @@
1
+ module GitHub
2
+ class Ldap
3
+ # Encapsulates common instrumentation behavior.
4
+ module Instrumentation
5
+ attr_reader :instrumentation_service
6
+ private :instrumentation_service
7
+
8
+ # Internal: Instrument a block with the defined instrumentation service.
9
+ #
10
+ # Yields the event payload if a block is given.
11
+ #
12
+ # Skips instrumentation if no service is set.
13
+ #
14
+ # Returns the return value of the block.
15
+ def instrument(event, payload = {})
16
+ payload = (payload || {}).dup
17
+ if instrumentation_service
18
+ instrumentation_service.instrument(event, payload) do |payload|
19
+ payload[:result] = yield(payload) if block_given?
20
+ end
21
+ else
22
+ yield(payload) if block_given?
23
+ end
24
+ end
25
+ private :instrument
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ require 'github/ldap/membership_validators/base'
2
+ require 'github/ldap/membership_validators/classic'
3
+ require 'github/ldap/membership_validators/recursive'
4
+ require 'github/ldap/membership_validators/active_directory'
5
+
6
+ module GitHub
7
+ class Ldap
8
+ # Provides various strategies for validating membership.
9
+ #
10
+ # For example:
11
+ #
12
+ # groups = domain.groups(%w(Engineering))
13
+ # validator = GitHub::Ldap::MembershipValidators::Classic.new(ldap, groups)
14
+ # validator.perform(entry) #=> true
15
+ #
16
+ module MembershipValidators; end
17
+ end
18
+ end
@@ -0,0 +1,56 @@
1
+ module GitHub
2
+ class Ldap
3
+ module MembershipValidators
4
+ ATTRS = %w(dn)
5
+ OID = "1.2.840.113556.1.4.1941"
6
+
7
+ # Validates membership using the ActiveDirectory "in chain" matching rule.
8
+ #
9
+ # The 1.2.840.113556.1.4.1941 matching rule (LDAP_MATCHING_RULE_IN_CHAIN)
10
+ # "walks the chain of ancestry in objects all the way to the root until
11
+ # it finds a match".
12
+ # Source: http://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx
13
+ #
14
+ # This means we have an efficient method of searching membership even in
15
+ # nested groups, performed on the server side.
16
+ class ActiveDirectory < Base
17
+ def perform(entry)
18
+ # short circuit validation if there are no groups to check against
19
+ return true if groups.empty?
20
+
21
+ # search for the entry on the condition that the entry is a member
22
+ # of one of the groups or their subgroups.
23
+ #
24
+ # Sets the entry to the base and scopes the search to the base,
25
+ # according to the source documentation, found here:
26
+ # http://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx
27
+ matched = ldap.search \
28
+ filter: membership_in_chain_filter(entry),
29
+ base: entry.dn,
30
+ scope: Net::LDAP::SearchScope_BaseObject,
31
+ attributes: ATTRS
32
+
33
+ # membership validated if entry was matched and returned as a result
34
+ matched.map(&:dn).include?(entry.dn)
35
+ end
36
+
37
+ # Internal: Constructs a membership filter using the "in chain"
38
+ # extended matching rule afforded by ActiveDirectory.
39
+ #
40
+ # Returns a Net::LDAP::Filter object.
41
+ def membership_in_chain_filter(entry)
42
+ group_dns.map do |dn|
43
+ Net::LDAP::Filter.ex("memberOf:#{OID}", dn)
44
+ end.reduce(:|)
45
+ end
46
+
47
+ # Internal: the group DNs to check against.
48
+ #
49
+ # Returns an Array of String DNs.
50
+ def group_dns
51
+ @group_dns ||= groups.map(&:dn)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,37 @@
1
+ module GitHub
2
+ class Ldap
3
+ module MembershipValidators
4
+ class Base
5
+
6
+ # Internal: The GitHub::Ldap object to search domains with.
7
+ attr_reader :ldap
8
+
9
+ # Internal: an Array of Net::LDAP::Entry group objects to validate with.
10
+ attr_reader :groups
11
+
12
+ # Public: Instantiate new validator.
13
+ #
14
+ # - ldap: GitHub::Ldap object
15
+ # - groups: Array of Net::LDAP::Entry group objects
16
+ def initialize(ldap, groups)
17
+ @ldap = ldap
18
+ @groups = groups
19
+ end
20
+
21
+ # Abstract: Performs the membership validation check.
22
+ #
23
+ # Returns Boolean whether the entry's membership is validated or not.
24
+ # def perform(entry)
25
+ # end
26
+
27
+ # Internal: Domains to search through.
28
+ #
29
+ # Returns an Array of GitHub::Ldap::Domain objects.
30
+ def domains
31
+ @domains ||= ldap.search_domains.map { |base| ldap.domain(base) }
32
+ end
33
+ private :domains
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ module GitHub
2
+ class Ldap
3
+ module MembershipValidators
4
+ # Validates membership using `GitHub::Ldap::Domain#membership`.
5
+ #
6
+ # This is a simple wrapper for existing functionality in order to expose
7
+ # it consistently with the new approach.
8
+ class Classic < Base
9
+ def perform(entry)
10
+ # short circuit validation if there are no groups to check against
11
+ return true if groups.empty?
12
+
13
+ domains.each do |domain|
14
+ membership = domain.membership(entry, group_names)
15
+
16
+ if !membership.empty?
17
+ entry[:groups] = membership
18
+ return true
19
+ end
20
+ end
21
+
22
+ false
23
+ end
24
+
25
+ # Internal: the group names to look up membership for.
26
+ #
27
+ # Returns an Array of String group names (CNs).
28
+ def group_names
29
+ @group_names ||= groups.map { |g| g[:cn].first }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end