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.
- checksums.yaml +4 -4
- data/.travis.yml +15 -2
- data/CHANGELOG.md +13 -0
- data/Gemfile +4 -0
- data/README.md +15 -1
- data/Rakefile +1 -1
- data/github-ldap.gemspec +2 -2
- data/lib/github/ldap.rb +55 -12
- data/lib/github/ldap/domain.rb +6 -2
- data/lib/github/ldap/filter.rb +15 -7
- data/lib/github/ldap/group.rb +1 -1
- data/lib/github/ldap/instrumentation.rb +28 -0
- data/lib/github/ldap/membership_validators.rb +18 -0
- data/lib/github/ldap/membership_validators/active_directory.rb +56 -0
- data/lib/github/ldap/membership_validators/base.rb +37 -0
- data/lib/github/ldap/membership_validators/classic.rb +34 -0
- data/lib/github/ldap/membership_validators/recursive.rb +93 -0
- data/lib/github/ldap/server.rb +2 -0
- data/script/changelog +29 -0
- data/script/cibuild-apacheds +7 -0
- data/script/cibuild-openldap +7 -0
- data/script/install-openldap +44 -0
- data/script/package +7 -0
- data/script/release +16 -0
- data/test/domain_test.rb +71 -89
- data/test/filter_test.rb +12 -1
- data/test/fixtures/common/seed.ldif +369 -0
- data/test/fixtures/openldap/memberof.ldif +33 -0
- data/test/fixtures/openldap/slapd.conf.ldif +67 -0
- data/test/fixtures/posixGroup.schema.ldif +34 -8
- data/test/group_test.rb +19 -25
- data/test/ldap_test.rb +28 -21
- data/test/membership_validators/active_directory_test.rb +68 -0
- data/test/membership_validators/classic_test.rb +51 -0
- data/test/membership_validators/recursive_test.rb +56 -0
- data/test/membership_validators_test.rb +46 -0
- data/test/posix_group_test.rb +25 -28
- data/test/support/vm/openldap/.gitignore +1 -0
- data/test/support/vm/openldap/README.md +32 -0
- data/test/support/vm/openldap/Vagrantfile +35 -0
- data/test/test_helper.rb +72 -10
- metadata +52 -27
- data/test/fixtures/github-with-looped-subgroups.ldif +0 -82
- data/test/fixtures/github-with-missing-entries.ldif +0 -85
- data/test/fixtures/github-with-posixGroups.ldif +0 -50
- data/test/fixtures/github-with-subgroups.ldif +0 -146
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 053a59e45e7ee63a06ee943da318bf9362455306
|
4
|
+
data.tar.gz: d2b435150ad904b336490fa9e5bc863b6ab6d38d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
4
|
-
|
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
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
<a href="https://travis-ci.org/github/github-ldap"></a>
|
1
|
+
<a href="https://travis-ci.org/github/github-ldap"></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
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.
|
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.
|
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
|
-
|
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({
|
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
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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.
|
data/lib/github/ldap/domain.rb
CHANGED
@@ -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
|
-
|
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.
|
data/lib/github/ldap/filter.rb
CHANGED
@@ -20,16 +20,18 @@ module GitHub
|
|
20
20
|
|
21
21
|
# Filter to check group membership.
|
22
22
|
#
|
23
|
-
# entry: finds groups this
|
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
|
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) }.
|
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(
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
|
data/lib/github/ldap/group.rb
CHANGED
@@ -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
|