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.
- 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">![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
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
|