github-ldap 1.3.3 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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