├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── github-ldap.gemspec
├── lib
└── github
│ ├── ldap.rb
│ └── ldap
│ ├── connection_cache.rb
│ ├── domain.rb
│ ├── filter.rb
│ ├── fixtures.ldif
│ ├── group.rb
│ ├── instrumentation.rb
│ ├── member_search.rb
│ ├── member_search
│ ├── active_directory.rb
│ ├── base.rb
│ ├── classic.rb
│ └── recursive.rb
│ ├── membership_validators.rb
│ ├── membership_validators
│ ├── active_directory.rb
│ ├── base.rb
│ ├── classic.rb
│ └── recursive.rb
│ ├── posix_group.rb
│ ├── referral_chaser.rb
│ ├── server.rb
│ ├── url.rb
│ ├── user_search
│ ├── active_directory.rb
│ └── default.rb
│ ├── virtual_attributes.rb
│ └── virtual_group.rb
├── script
├── changelog
├── cibuild-apacheds
├── cibuild-openldap
├── install-openldap
├── package
└── release
└── test
├── connection_cache_test.rb
├── domain_test.rb
├── filter_test.rb
├── fixtures
├── common
│ └── seed.ldif
├── openldap
│ ├── memberof.ldif
│ └── slapd.conf.ldif
└── posixGroup.schema.ldif
├── group_test.rb
├── ldap_test.rb
├── member_search
├── active_directory_test.rb
├── classic_test.rb
└── recursive_test.rb
├── membership_validators
├── active_directory_test.rb
├── classic_test.rb
└── recursive_test.rb
├── posix_group_test.rb
├── referral_chaser_test.rb
├── support
└── vm
│ ├── activedirectory
│ ├── .gitignore
│ ├── README.md
│ ├── env.sh.example
│ └── reset-env.sh
│ └── openldap
│ ├── .gitignore
│ ├── README.md
│ └── Vagrantfile
├── test_helper.rb
├── url_test.rb
└── user_search
├── active_directory_test.rb
└── default_test.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .yardoc
6 | Gemfile.lock
7 | InstalledFiles
8 | _yardoc
9 | coverage
10 | doc/
11 | lib/bundler/man
12 | pkg
13 | rdoc
14 | spec/reports
15 | test/tmp
16 | test/version_tmp
17 | tmp
18 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.0.0
4 | - 2.1.0
5 |
6 | env:
7 | - TESTENV=openldap
8 | - TESTENV=apacheds
9 |
10 | # https://docs.travis-ci.com/user/hosts/
11 | addons:
12 | hosts:
13 | - ad1.ghe.dev
14 | - ad2.ghe.dev
15 |
16 | before_install:
17 | - echo "deb http://ftp.br.debian.org/debian stable main" | sudo tee -a /etc/apt/sources.list
18 | - sudo apt-get update
19 |
20 | install:
21 | - if [ "$TESTENV" = "openldap" ]; then ./script/install-openldap; fi
22 | - bundle install
23 |
24 | script:
25 | - ./script/cibuild-$TESTENV
26 |
27 | matrix:
28 | fast_finish: true
29 | notifications:
30 | email: false
31 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | # v1.10.1
4 |
5 | * Bump net-ldap to 0.16.0
6 |
7 | # v1.10.0
8 |
9 | * Bump net-ldap to 0.15.0 [#92](https://github.com/github/github-ldap/pull/92)
10 |
11 | # v1.9.0
12 |
13 | * Update net-ldap dependency to `~> 0.11.0` [#84](https://github.com/github/github-ldap/pull/84)
14 |
15 | # v1.8.2
16 |
17 | * Ignore case when comparing ActiveDirectory DNs [#82](https://github.com/github/github-ldap/pull/82)
18 |
19 | # v1.8.1
20 |
21 | * Expand supported ActiveDirectory capabilities to include Windows Server 2003 [#80](https://github.com/github/github-ldap/pull/80)
22 |
23 | # v1.8.0
24 |
25 | * Optimize Recursive *Member Search* strategy [#78](https://github.com/github/github-ldap/pull/78)
26 |
27 | # v1.7.1
28 |
29 | * Add Active Directory group filter [#75](https://github.com/github/github-ldap/pull/75)
30 |
31 | ## v1.7.0
32 |
33 | * Accept `:depth` option for Recursive membership validator strategy instance [#73](https://github.com/github/github-ldap/pull/73)
34 | * Deprecate `depth` argument to `Recursive` membership validator `perform` method
35 | * Bump net-ldap dependency to 0.10.0 at minimum [#72](https://github.com/github/github-ldap/pull/72)
36 |
37 | ## v1.6.0
38 |
39 | * Expose `GitHub::Ldap::Group.group?` for testing if entry is a group [#67](https://github.com/github/github-ldap/pull/67)
40 | * Add *Member Search* strategies [#64](https://github.com/github/github-ldap/pull/64) [#68](https://github.com/github/github-ldap/pull/68) [#69](https://github.com/github/github-ldap/pull/69)
41 | * Simplify *Member Search* and *Membership Validation* search strategy configuration, detection, and default behavior [#70](https://github.com/github/github-ldap/pull/70)
42 |
43 | ## v1.5.0
44 |
45 | * Automatically detect membership validator strategy by default [#58](https://github.com/github/github-ldap/pull/58) [#62](https://github.com/github/github-ldap/pull/62)
46 | * Document local integration testing with Active Directory [#61](https://github.com/github/github-ldap/pull/61)
47 |
48 | ## v1.4.0
49 |
50 | * Document constructor options [#57](https://github.com/github/github-ldap/pull/57)
51 | * [CI] Add Vagrant box for running tests against OpenLDAP locally [#55](https://github.com/github/github-ldap/pull/55)
52 | * Run all tests, including those in subdirectories [#54](https://github.com/github/github-ldap/pull/54)
53 | * Add ActiveDirectory membership validator [#52](https://github.com/github/github-ldap/pull/52)
54 | * Merge dev-v2 branch into master [#50](https://github.com/github/github-ldap/pull/50)
55 | * Pass through search options for GitHub::Ldap::Domain#user? [#51](https://github.com/github/github-ldap/pull/51)
56 | * Fix membership validation tests [#49](https://github.com/github/github-ldap/pull/49)
57 | * Add CI build for OpenLDAP integration [#48](https://github.com/github/github-ldap/pull/48)
58 | * Membership Validators [#45](https://github.com/github/github-ldap/pull/45)
59 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in github-ldap.gemspec
4 | gemspec
5 |
6 | group :test, :development do
7 | gem "byebug", :platforms => [:mri_20, :mri_21]
8 | end
9 |
10 | group :test do
11 | gem "mocha"
12 | end
13 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 David Calavera
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Github::Ldap
4 |
5 | GitHub-Ldap is a wrapper on top of Net::LDAP to make it human friendly.
6 |
7 | ## Installation
8 |
9 | Add this line to your application's Gemfile:
10 |
11 | gem 'github-ldap'
12 |
13 | And then execute:
14 |
15 | $ bundle
16 |
17 | Or install it yourself as:
18 |
19 | $ gem install github-ldap
20 |
21 | ## Usage
22 |
23 | ### Initialization
24 |
25 | GitHub-Ldap let you use an external ldap server to authenticate your users with.
26 |
27 | There are a few configuration options required to use this adapter:
28 |
29 | * host: is the host address where the ldap server lives.
30 | * port: is the port where the ldap server lives.
31 | * hosts: (optional) an enumerable of pairs of hosts and corresponding ports with which to attempt opening connections (default [[host, port]]). Overrides host and port if set.
32 | * encryption: is the encryption protocol, disabled by default. The valid options are `ssl` and `tls`.
33 | * uid: is the field name in the ldap server used to authenticate your users, in ActiveDirectory this is `sAMAccountName`.
34 |
35 | Using administrator credentials is optional but recommended. You can pass those credentials with these two options:
36 |
37 | * admin_user: is the the ldap administrator user dn.
38 | * admin_password: is the password for the administrator user.
39 |
40 | Initialize a new adapter using those required options:
41 |
42 | ```ruby
43 | ldap = GitHub::Ldap.new options
44 | ```
45 |
46 | See GitHub::Ldap#initialize for additional options.
47 |
48 | ### Querying
49 |
50 | Searches are performed against an individual domain base, so the first step is to get a new `GitHub::Ldap::Domain` object for the connection:
51 |
52 | ```ruby
53 | ldap = GitHub::Ldap.new options
54 | domain = ldap.domain("dc=github,dc=com")
55 | ```
56 |
57 | When we have the domain, we can check if a user can log in with a given password:
58 |
59 | ```ruby
60 | domain.valid_login? 'calavera', 'secret'
61 | ```
62 |
63 | Or whether a user is member of the given groups:
64 |
65 | ```ruby
66 | entry = ldap.domain('uid=calavera,dc=github,dc=com').bind
67 | domain.is_member? entry, %w(Enterprise)
68 | ```
69 |
70 | ### Virtual Attributes
71 |
72 | Some LDAP servers have support for virtual attributes, or overlays. These allow to perform queries more efficiently on the server.
73 |
74 | To enable virtual attributes you can set the option `virtual_attributes` initializing the ldap connection.
75 | We use our default set of virtual names if this option is just set to `true`.
76 |
77 | ```ruby
78 | ldap = GitHub::Ldap.new {virtual_attributes: true}
79 | ```
80 |
81 | You can also override our defaults by providing your server mappings into a Hash.
82 | The only mapping supported for now is to check virtual membership of individuals in groups.
83 |
84 | ```ruby
85 | ldap = GitHub::Ldap.new {virtual_attributes: {virtual_membership: 'memberOf'}}
86 | ```
87 |
88 | ### Testing support
89 |
90 | GitHub-Ldap uses [ladle](https://github.com/NUBIC/ladle) for testing. Ladle is not required by default, so you'll need to add it to your gemfile separatedly and require it.
91 |
92 | Once you have it installed you can start the testing ldap server in the setup phase for your tests:
93 |
94 | ```ruby
95 | require 'github/ldap/server'
96 |
97 | def setup
98 | GitHub::Ldap.start_server
99 | end
100 |
101 | def teardown
102 | GitHub::Ldap.stop_server
103 | end
104 | ```
105 |
106 | GitHub-Ldap includes a set of configured users for testing, but you can provide your own users into a ldif file:
107 |
108 | ```ruby
109 | def setup
110 | GitHub::Ldap.start_server \
111 | user_fixtures: ldif_path
112 | end
113 | ```
114 |
115 | If you provide your own user fixtures, you'll probably need to change the default user domain, the administrator name and her password:
116 |
117 | ```ruby
118 | def setup
119 | GitHub::Ldap.start_server \
120 | user_fixtures: ldif_path,
121 | user_domain: 'dc=evilcorp,dc=com'
122 | admin_user: 'uid=eviladmin,dc=evilcorp,dc=com',
123 | admin_password: 'correct horse battery staple'
124 | end
125 | ```
126 |
127 | ## Contributing
128 |
129 | 1. Fork it
130 | 2. Create your feature branch (`git checkout -b my-new-feature`)
131 | 3. Commit your changes (`git commit -am 'Add some feature'`)
132 | 4. Push to the branch (`git push origin my-new-feature`)
133 | 5. Create new Pull Request
134 |
135 | ## Releasing
136 |
137 | This section is for gem maintainers to cut a new version of the gem. See
138 | [jch/release-scripts](https://github.com/jch/release-scripts) for original
139 | source of release scripts.
140 |
141 | * Create a new branch from `master` named `release-x.y.z`, where `x.y.z` is the version to be released
142 | * Update `github-ldap.gemspec` to x.y.z following [semver](http://semver.org)
143 | * Run `script/changelog` and paste the draft into `CHANGELOG.md`. Edit as needed
144 | * Create pull request to solict feedback
145 | * After merging the pull request, on the master branch, run `script/release`
146 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require 'rake/testtask'
3 |
4 | Rake::TestTask.new do |t|
5 | t.libs << "test"
6 | t.pattern = "test/**/*_test.rb"
7 | end
8 |
9 | task :default => :test
10 |
--------------------------------------------------------------------------------
/github-ldap.gemspec:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = "github-ldap"
5 | spec.version = "1.10.1"
6 | spec.authors = ["David Calavera", "Matt Todd"]
7 | spec.email = ["david.calavera@gmail.com", "chiology@gmail.com"]
8 | spec.description = %q{LDAP authentication for humans}
9 | spec.summary = %q{LDAP client authentication wrapper without all the boilerplate}
10 | spec.homepage = "https://github.com/github/github-ldap"
11 | spec.license = "MIT"
12 |
13 | spec.files = `git ls-files`.split($/)
14 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
15 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16 | spec.require_paths = ["lib"]
17 |
18 | spec.add_dependency 'net-ldap', '> 0.16.0'
19 |
20 | spec.add_development_dependency "bundler", "~> 1.3"
21 | spec.add_development_dependency 'ladle'
22 | spec.add_development_dependency 'minitest', '~> 5'
23 | spec.add_development_dependency "rake"
24 | end
25 |
--------------------------------------------------------------------------------
/lib/github/ldap.rb:
--------------------------------------------------------------------------------
1 | require 'net/ldap'
2 | require 'forwardable'
3 |
4 | require 'github/ldap/filter'
5 | require 'github/ldap/domain'
6 | require 'github/ldap/group'
7 | require 'github/ldap/posix_group'
8 | require 'github/ldap/virtual_group'
9 | require 'github/ldap/virtual_attributes'
10 | require 'github/ldap/instrumentation'
11 | require 'github/ldap/member_search'
12 | require 'github/ldap/membership_validators'
13 | require 'github/ldap/user_search/default'
14 | require 'github/ldap/user_search/active_directory'
15 | require 'github/ldap/connection_cache'
16 | require 'github/ldap/referral_chaser'
17 | require 'github/ldap/url'
18 |
19 | module GitHub
20 | class Ldap
21 | include Instrumentation
22 |
23 | extend Forwardable
24 |
25 | # Internal: The capability required to use ActiveDirectory features.
26 | # See: http://msdn.microsoft.com/en-us/library/cc223359.aspx.
27 | ACTIVE_DIRECTORY_V51_OID = "1.2.840.113556.1.4.1670".freeze
28 |
29 | # Utility method to get the last operation result with a human friendly message.
30 | #
31 | # Returns an OpenStruct with `code` and `message`.
32 | # If `code` is 0, the operation succeeded and there is no message.
33 | def_delegator :@connection, :get_operation_result, :last_operation_result
34 |
35 | # Utility method to bind entries in the ldap server.
36 | #
37 | # It takes the same arguments than Net::LDAP::Connection#bind.
38 | # Returns a Net::LDAP::Entry if the operation succeeded.
39 | def_delegator :@connection, :bind
40 |
41 | # Public - Opens a connection to the server and keeps it open for the
42 | # duration of the block.
43 | #
44 | # Returns the return value of the block.
45 | def_delegator :@connection, :open
46 | def_delegator :@connection, :host
47 |
48 | attr_reader :uid, :search_domains, :virtual_attributes,
49 | :membership_validator,
50 | :member_search_strategy,
51 | :instrumentation_service,
52 | :user_search_strategy,
53 | :connection,
54 | :admin_user,
55 | :admin_password,
56 | :port
57 |
58 | # Build a new GitHub::Ldap instance
59 | #
60 | # ## Connection
61 | #
62 | # host: required string ldap server host address
63 | # port: required string or number ldap server port
64 | # hosts: an enumerable of pairs of hosts and corresponding ports with
65 | # which to attempt opening connections (default [[host, port]]). Overrides
66 | # host and port if set.
67 | # encryption: optional string. `ssl` or `tls`. nil by default
68 | # tls_options: optional hash with TLS options for encrypted connections.
69 | # Empty by default. See http://ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html
70 | # for available values
71 | # admin_user: optional string ldap administrator user dn for authentication
72 | # admin_password: optional string ldap administrator user password
73 | #
74 | # ## Behavior
75 | #
76 | # uid: optional field name used to authenticate users. Defaults to `sAMAccountName` (what ActiveDirectory uses)
77 | # virtual_attributes: optional. boolean true to use server's virtual attributes. Hash to specify custom mapping. Default false.
78 | # recursive_group_search_fallback: optional boolean whether membership checks should recurse into nested groups when virtual attributes aren't enabled. Default false.
79 | # posix_support: optional boolean `posixGroup` support. Default true.
80 | # search_domains: optional array of string bases to search through
81 | #
82 | # ## Diagnostics
83 | #
84 | # instrumentation_service: optional ActiveSupport::Notifications compatible object
85 | #
86 | def initialize(options = {})
87 | @uid = options[:uid] || "sAMAccountName"
88 |
89 | # Keep a reference to these as default auth for a Global Catalog if needed
90 | @admin_user = options[:admin_user]
91 | @admin_password = options[:admin_password]
92 | @port = options[:port]
93 |
94 | @connection = Net::LDAP.new({
95 | host: options[:host],
96 | port: options[:port],
97 | hosts: options[:hosts],
98 | instrumentation_service: options[:instrumentation_service]
99 | })
100 |
101 | if options[:admin_user] && options[:admin_password]
102 | @connection.authenticate(options[:admin_user], options[:admin_password])
103 | end
104 |
105 | if encryption = check_encryption(options[:encryption], options[:tls_options])
106 | @connection.encryption(encryption)
107 | end
108 |
109 | configure_virtual_attributes(options[:virtual_attributes])
110 |
111 | # enable fallback recursive group search unless option is false
112 | @recursive_group_search_fallback = (options[:recursive_group_search_fallback] != false)
113 |
114 | # enable posixGroup support unless option is false
115 | @posix_support = (options[:posix_support] != false)
116 |
117 | # search_domains is a connection of bases to perform searches
118 | # when a base is not explicitly provided.
119 | @search_domains = Array(options[:search_domains])
120 |
121 | # configure both the membership validator and the member search strategies
122 | configure_search_strategy(options[:search_strategy])
123 |
124 | # configure the strategy used by Domain#user? to look up a user entry for login
125 | configure_user_search_strategy(options[:user_search_strategy])
126 |
127 | # enables instrumenting queries
128 | @instrumentation_service = options[:instrumentation_service]
129 | end
130 |
131 | # Public - Whether membership checks should recurse into nested groups when
132 | # virtual attributes aren't enabled. The fallback search has poor
133 | # performance characteristics in some cases, in which case this should be
134 | # disabled by passing :recursive_group_search_fallback => false.
135 | #
136 | # Returns true or false.
137 | def recursive_group_search_fallback?
138 | @recursive_group_search_fallback
139 | end
140 |
141 | # Public - Whether membership checks should include posixGroup filter
142 | # conditions on `memberUid`. Configurable since some LDAP servers don't
143 | # handle unsupported attribute queries gracefully.
144 | #
145 | # Enable by passing :posix_support => true.
146 | #
147 | # Returns true, false, or nil (assumed false).
148 | def posix_support_enabled?
149 | @posix_support
150 | end
151 |
152 | # Public - Utility method to check if the connection with the server can be stablished.
153 | # It tries to bind with the ldap auth default configuration.
154 | #
155 | # Returns an OpenStruct with `code` and `message`.
156 | # If `code` is 0, the operation succeeded and there is no message.
157 | def test_connection
158 | @connection.bind
159 | last_operation_result
160 | end
161 |
162 | # Public - Creates a new domain object to perform operations
163 | #
164 | # base_name: is the dn of the base root.
165 | #
166 | # Returns a new Domain object.
167 | def domain(base_name)
168 | Domain.new(self, base_name, @uid)
169 | end
170 |
171 | # Public - Creates a new group object to perform operations
172 | #
173 | # base_name: is the dn of the base root.
174 | #
175 | # Returns a new Group object.
176 | # Returns nil if the dn is not in the server.
177 | def group(base_name)
178 | entry = domain(base_name).bind
179 | return unless entry
180 |
181 | load_group(entry)
182 | end
183 |
184 | # Public - Create a new group object based on a Net::LDAP::Entry.
185 | #
186 | # group_entry: is a Net::LDAP::Entry.
187 | #
188 | # Returns a Group, PosixGroup or VirtualGroup object.
189 | def load_group(group_entry)
190 | if @virtual_attributes.enabled?
191 | VirtualGroup.new(self, group_entry)
192 | elsif posix_support_enabled? && PosixGroup.valid?(group_entry)
193 | PosixGroup.new(self, group_entry)
194 | else
195 | Group.new(self, group_entry)
196 | end
197 | end
198 |
199 | # Public - Search entries in the ldap server.
200 | #
201 | # options: is a hash with the same options that Net::LDAP::Connection#search supports.
202 | # block: is an optional block to pass to the search.
203 | #
204 | # Returns an Array of Net::LDAP::Entry.
205 | def search(options, &block)
206 | instrument "search.github_ldap", options.dup do |payload|
207 | result =
208 | if options[:base]
209 | @connection.search(options, &block)
210 | else
211 | search_domains.each_with_object([]) do |base, result|
212 | rs = @connection.search(options.merge(:base => base), &block)
213 | result.concat Array(rs) unless rs == false
214 | end
215 | end
216 |
217 | return [] if result == false
218 | Array(result)
219 | end
220 | end
221 |
222 | # Internal: Searches the host LDAP server's Root DSE for capabilities and
223 | # extensions.
224 | #
225 | # Returns a Net::LDAP::Entry object.
226 | def capabilities
227 | @capabilities ||=
228 | instrument "capabilities.github_ldap" do |payload|
229 | begin
230 | @connection.search_root_dse
231 | rescue Net::LDAP::Error => error
232 | payload[:error] = error
233 | # stubbed result
234 | Net::LDAP::Entry.new
235 | end
236 | end
237 | end
238 |
239 | # Internal - Determine whether to use encryption or not.
240 | #
241 | # encryption: is the encryption method, either 'ssl', 'tls', 'simple_tls' or 'start_tls'.
242 | # tls_options: is the options hash for tls encryption method
243 | #
244 | # Returns the real encryption type.
245 | def check_encryption(encryption, tls_options = {})
246 | return unless encryption
247 |
248 | tls_options ||= {}
249 | case encryption.downcase.to_sym
250 | when :ssl, :simple_tls
251 | { method: :simple_tls, tls_options: tls_options }
252 | when :tls, :start_tls
253 | { method: :start_tls, tls_options: tls_options }
254 | end
255 | end
256 |
257 | # Internal - Configure virtual attributes for this server.
258 | # If the option is `true`, we'll use the default virual attributes.
259 | # If it's a Hash we'll map the attributes in the hash.
260 | #
261 | # attributes: is the option set when Ldap is initialized.
262 | #
263 | # Returns a VirtualAttributes.
264 | def configure_virtual_attributes(attributes)
265 | @virtual_attributes = if attributes == true
266 | VirtualAttributes.new(true)
267 | elsif attributes.is_a?(Hash)
268 | VirtualAttributes.new(true, attributes)
269 | else
270 | VirtualAttributes.new(false)
271 | end
272 | end
273 |
274 | # Internal: Configure the member search and membership validation strategies.
275 | #
276 | # TODO: Inline the logic in these two methods here.
277 | #
278 | # Returns nothing.
279 | def configure_search_strategy(strategy = nil)
280 | # configure which strategy should be used to validate user membership
281 | configure_membership_validation_strategy(strategy)
282 |
283 | # configure which strategy should be used for member search
284 | configure_member_search_strategy(strategy)
285 | end
286 |
287 | # Internal: Configure the membership validation strategy.
288 | #
289 | # If no known strategy is provided, detects ActiveDirectory capabilities or
290 | # falls back to the Recursive strategy by default.
291 | #
292 | # Returns the membership validator strategy Class.
293 | def configure_membership_validation_strategy(strategy = nil)
294 | @membership_validator =
295 | case strategy.to_s
296 | when "classic"
297 | GitHub::Ldap::MembershipValidators::Classic
298 | when "recursive"
299 | GitHub::Ldap::MembershipValidators::Recursive
300 | when "active_directory"
301 | GitHub::Ldap::MembershipValidators::ActiveDirectory
302 | else
303 | # fallback to detection, defaulting to recursive strategy
304 | if active_directory_capability?
305 | GitHub::Ldap::MembershipValidators::ActiveDirectory
306 | else
307 | GitHub::Ldap::MembershipValidators::Recursive
308 | end
309 | end
310 | end
311 |
312 | # Internal: Set the user search strategy that will be used by
313 | # Domain#user?.
314 | #
315 | # strategy - Can be either 'default' or 'global_catalog'.
316 | # 'default' strategy will search the configured
317 | # domain controller with a search base relative
318 | # to the controller's domain context.
319 | # 'global_catalog' will search the entire forest
320 | # using Active Directory's Global Catalog
321 | # functionality.
322 | def configure_user_search_strategy(strategy)
323 | @user_search_strategy =
324 | case strategy.to_s
325 | when "default"
326 | GitHub::Ldap::UserSearch::Default.new(self)
327 | when "global_catalog"
328 | GitHub::Ldap::UserSearch::ActiveDirectory.new(self)
329 | else
330 | GitHub::Ldap::UserSearch::Default.new(self)
331 | end
332 | end
333 |
334 | # Internal: Configure the member search strategy.
335 | #
336 | #
337 | # If no known strategy is provided, detects ActiveDirectory capabilities or
338 | # falls back to the Recursive strategy by default.
339 | #
340 | # Returns the selected strategy Class.
341 | def configure_member_search_strategy(strategy = nil)
342 | @member_search_strategy =
343 | case strategy.to_s
344 | when "classic"
345 | GitHub::Ldap::MemberSearch::Classic
346 | when "recursive"
347 | GitHub::Ldap::MemberSearch::Recursive
348 | when "active_directory"
349 | GitHub::Ldap::MemberSearch::ActiveDirectory
350 | else
351 | # fallback to detection, defaulting to recursive strategy
352 | if active_directory_capability?
353 | GitHub::Ldap::MemberSearch::ActiveDirectory
354 | else
355 | GitHub::Ldap::MemberSearch::Recursive
356 | end
357 | end
358 | end
359 |
360 | # Internal: Detect whether the LDAP host is an ActiveDirectory server.
361 | #
362 | # See: http://msdn.microsoft.com/en-us/library/cc223359.aspx.
363 | #
364 | # Returns true if the host is an ActiveDirectory server, false otherwise.
365 | def active_directory_capability?
366 | capabilities[:supportedcapabilities].include?(ACTIVE_DIRECTORY_V51_OID)
367 | end
368 | private :active_directory_capability?
369 | end
370 | end
371 |
--------------------------------------------------------------------------------
/lib/github/ldap/connection_cache.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 |
4 | # A simple cache of GitHub::Ldap objects to prevent creating multiple
5 | # instances of connections that point to the same URI/host.
6 | class ConnectionCache
7 |
8 | # Public - Create or return cached instance of GitHub::Ldap created with options,
9 | # where the cache key is the value of options[:host].
10 | #
11 | # options - Initialization attributes suitable for creating a new connection with
12 | # GitHub::Ldap.new(options)
13 | #
14 | # Returns true or false.
15 | def self.get_connection(options={})
16 | @cache ||= self.new
17 | @cache.get_connection(options)
18 | end
19 |
20 | def get_connection(options)
21 | @connections ||= {}
22 | @connections[options[:host]] ||= GitHub::Ldap.new(options)
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/github/ldap/domain.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | # A domain represents the base object for an ldap tree.
4 | # It encapsulates the operations that you can perform against a tree, authenticating users, for instance.
5 | #
6 | # This makes possible to reuse a server connection to perform operations with two different domain bases.
7 | #
8 | # To get a domain, you'll need to create a `Ldap` object and then call the method `domain` with the name of the base.
9 | #
10 | # For example:
11 | #
12 | # domain = GitHub::Ldap.new(options).domain("dc=github,dc=com")
13 | #
14 | class Domain
15 | include Filter
16 |
17 | def initialize(ldap, base_name, uid)
18 | @ldap, @base_name, @uid = ldap, base_name, uid
19 | end
20 |
21 | # List all groups under this tree, including subgroups.
22 | #
23 | # Returns a list of ldap entries.
24 | def all_groups
25 | search(filter: ALL_GROUPS_FILTER)
26 | end
27 |
28 | # List all groups under this tree that match the query.
29 | #
30 | # query: is the partial name to filter for.
31 | # opts: additional options to filter with. It's specially recommended to restrict this search by size.
32 | # block: is an optional block to pass to the search.
33 | #
34 | # Returns a list of ldap entries.
35 | def filter_groups(query, opts = {}, &block)
36 | search(opts.merge(filter: group_contains_filter(query)), &block)
37 | end
38 |
39 | # List the groups in the ldap server that match the configured ones.
40 | #
41 | # group_names: is an array of group CNs.
42 | #
43 | # Returns a list of ldap entries for the configured groups.
44 | def groups(group_names)
45 | search(filter: group_filter(group_names))
46 | end
47 |
48 | # List the groups that a user is member of.
49 | #
50 | # user_entry: is the entry for the user in the server.
51 | # group_names: is an array of group CNs.
52 | #
53 | # Return an Array with the groups that the given user is member of that belong to the given group list.
54 | def membership(user_entry, group_names)
55 | if @ldap.virtual_attributes.enabled? || @ldap.recursive_group_search_fallback?
56 | all_groups = search(filter: group_filter(group_names))
57 | groups_map = all_groups.each_with_object({}) {|entry, hash| hash[entry.dn] = entry}
58 |
59 | if @ldap.virtual_attributes.enabled?
60 | member_of = groups_map.keys & user_entry[@ldap.virtual_attributes.virtual_membership]
61 | member_of.map {|dn| groups_map[dn]}
62 | else # recursive group search fallback
63 | groups_map.each_with_object([]) do |(dn, group_entry), acc|
64 | acc << group_entry if @ldap.load_group(group_entry).is_member?(user_entry)
65 | end
66 | end
67 | else
68 | # fallback to non-recursive group membership search
69 | filter = member_filter(user_entry)
70 |
71 | # include memberUid filter if enabled and entry has a UID set
72 | if @ldap.posix_support_enabled? && !user_entry[@ldap.uid].empty?
73 | filter |= posix_member_filter(user_entry, @ldap.uid)
74 | end
75 |
76 | filter &= group_filter(group_names)
77 | search(filter: filter)
78 | end
79 | end
80 |
81 | # Check if the user is include in any of the configured groups.
82 | #
83 | # user_entry: is the entry for the user in the server.
84 | # group_names: is an array of group CNs.
85 | #
86 | # Returns true if the user belongs to any of the groups.
87 | # Returns false otherwise.
88 | def is_member?(user_entry, group_names)
89 | return true if group_names.nil?
90 | return true if group_names.empty?
91 |
92 | user_membership = membership(user_entry, group_names)
93 |
94 | !user_membership.empty?
95 | end
96 |
97 | # Check if the user credentials are valid.
98 | #
99 | # login: is the user's login.
100 | # password: is the user's password.
101 | #
102 | # Returns a Ldap::Entry if the credentials are valid.
103 | # Returns nil if the credentials are invalid.
104 | def valid_login?(login, password)
105 | if user = user?(login) and auth(user, password)
106 | return user
107 | end
108 | end
109 |
110 | # Check if a user exists based in the `uid`.
111 | #
112 | # login: is the user's login
113 | # search_options: Net::LDAP#search compatible options to pass through
114 | #
115 | # Returns the user if the login matches any `uid`.
116 | # Returns nil if there are no matches.
117 | def user?(login, search_options = {})
118 | @ldap.user_search_strategy.perform(login, @base_name, @uid, search_options).first
119 | end
120 |
121 | # Check if a user can be bound with a password.
122 | #
123 | # user: is a ldap entry representing the user.
124 | # password: is the user's password.
125 | #
126 | # Returns true if the user can be bound.
127 | def auth(user, password)
128 | @ldap.bind(method: :simple, username: user.dn, password: password)
129 | end
130 |
131 | # Authenticate a user with the ldap server.
132 | #
133 | # login: is the user's login. This method doesn't accept email identifications.
134 | # password: is the user's password.
135 | # group_names: is an array of group CNs.
136 | #
137 | # Returns the user info if the credentials are valid and there are no groups configured.
138 | # Returns the user info if the credentials are valid and the user belongs to a configured group.
139 | # Returns nil if the credentials are invalid
140 | def authenticate!(login, password, group_names = nil)
141 | user = valid_login?(login, password)
142 |
143 | return user if user && is_member?(user, group_names)
144 | end
145 |
146 | # Search entries using this domain as base.
147 | #
148 | # options: is a Hash with the options for the search. The base option is always overriden.
149 | # block: is an optional block to pass to the search.
150 | #
151 | # Returns an array with the entries found.
152 | def search(options, &block)
153 | options[:base] = @base_name
154 | options[:attributes] ||= []
155 | options[:paged_searches_supported] = true
156 |
157 | @ldap.search(options, &block)
158 | end
159 |
160 | # Get the entry for this domain.
161 | #
162 | # Returns a Net::LDAP::Entry
163 | def bind(options = {})
164 | options[:size] = 1
165 | options[:scope] = Net::LDAP::SearchScope_BaseObject
166 | options[:attributes] ||= []
167 | search(options).first
168 | end
169 | end
170 | end
171 | end
172 |
--------------------------------------------------------------------------------
/lib/github/ldap/filter.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | module Filter
4 | ALL_GROUPS_FILTER = Net::LDAP::Filter.eq("objectClass", "groupOfNames") |
5 | Net::LDAP::Filter.eq("objectClass", "groupOfUniqueNames") |
6 | Net::LDAP::Filter.eq("objectClass", "posixGroup") |
7 | Net::LDAP::Filter.eq("objectClass", "group")
8 |
9 | MEMBERSHIP_NAMES = %w(member uniqueMember)
10 |
11 | # Filter to get the configured groups in the ldap server.
12 | # Takes the list of the group names and generate a filter for the groups
13 | # with cn that match.
14 | #
15 | # group_names: is an array of group CNs.
16 | #
17 | # Returns a Net::LDAP::Filter.
18 | def group_filter(group_names)
19 | group_names.map {|g| Net::LDAP::Filter.eq("cn", g)}.reduce(:|)
20 | end
21 |
22 | # Filter to check group membership.
23 | #
24 | # entry: finds groups this entry is a member of (optional)
25 | # Expects a Net::LDAP::Entry or String DN.
26 | #
27 | # Returns a Net::LDAP::Filter.
28 | def member_filter(entry = nil)
29 | if entry
30 | entry = entry.dn if entry.respond_to?(:dn)
31 | MEMBERSHIP_NAMES.
32 | map {|n| Net::LDAP::Filter.eq(n, entry) }.reduce(:|)
33 | else
34 | MEMBERSHIP_NAMES.
35 | map {|n| Net::LDAP::Filter.pres(n) }. reduce(:|)
36 | end
37 | end
38 |
39 | # Filter to check group membership for posixGroups.
40 | #
41 | # Used by Domain#membership when posix_support_enabled? is true.
42 | #
43 | # entry: finds groups this Net::LDAP::Entry is a member of
44 | # uid_attr: specifies the memberUid attribute to match with
45 | #
46 | # Returns a Net::LDAP::Filter or nil if no entry has no UID set.
47 | def posix_member_filter(entry_or_uid, uid_attr = nil)
48 | case entry_or_uid
49 | when Net::LDAP::Entry
50 | entry = entry_or_uid
51 | if !entry[uid_attr].empty?
52 | entry[uid_attr].map { |uid| Net::LDAP::Filter.eq("memberUid", uid) }.
53 | reduce(:|)
54 | end
55 | when String
56 | Net::LDAP::Filter.eq("memberUid", entry_or_uid)
57 | end
58 | end
59 |
60 | # Filter to map a uid with a login.
61 | # It escapes the login before creating the filter.
62 | #
63 | # uid: the entry field to map.
64 | # login: the login to map.
65 | #
66 | # Returns a Net::LDAP::Filter.
67 | def login_filter(uid, login)
68 | Net::LDAP::Filter.eq(uid, Net::LDAP::Filter.escape(login))
69 | end
70 |
71 | # Filter groups that match a query cn.
72 | #
73 | # query: is a string to match the cn with.
74 | #
75 | # Returns a Net::LDAP::Filter.
76 | def group_contains_filter(query)
77 | Net::LDAP::Filter.contains("cn", query) & ALL_GROUPS_FILTER
78 | end
79 |
80 | # Filter to get all the members of a group using the virtual attribute `memberOf`.
81 | #
82 | # group_dn: is the group dn to look members for.
83 | # attr: is the membership attribute.
84 | #
85 | # Returns a Net::LDAP::Filter
86 | def members_of_group(group_dn, attr = 'memberOf')
87 | Net::LDAP::Filter.eq(attr, group_dn)
88 | end
89 |
90 | # Filter to get all the members of a group that are groups using the virtual attribute `memberOf`.
91 | #
92 | # group_dn: is the group dn to look members for.
93 | # attr: is the membership attribute.
94 | #
95 | # Returns a Net::LDAP::Filter
96 | def subgroups_of_group(group_dn, attr = 'memberOf')
97 | Net::LDAP::Filter.eq(attr, group_dn) & ALL_GROUPS_FILTER
98 | end
99 |
100 | # Filter to get all the members of a group which uid is included in `memberUid`.
101 | #
102 | # uids: is an array with all the uids to search.
103 | # uid_attr: is the names of the uid attribute in the directory.
104 | #
105 | # Returns a Net::LDAP::Filter
106 | def all_members_by_uid(uids, uid_attr)
107 | uids.map {|uid| Net::LDAP::Filter.eq(uid_attr, uid)}.reduce(:|)
108 | end
109 | end
110 | end
111 | end
112 |
--------------------------------------------------------------------------------
/lib/github/ldap/fixtures.ldif:
--------------------------------------------------------------------------------
1 | version: 1
2 |
3 | dn: ou=Group,dc=github,dc=com
4 | objectclass: organizationalUnit
5 | ou: Group
6 |
7 | # Groups
8 | dn: cn=People,ou=Group,dc=github,dc=com
9 | cn: People
10 | objectClass: groupOfNames
11 | member: uid=sean,dc=github,dc=com
12 |
13 | dn: cn=Enterprise,ou=Group,dc=github,dc=com
14 | cn: Enterprise
15 | objectClass: groupOfNames
16 | member: uid=calavera,dc=github,dc=com
17 |
18 | # Users
19 | dn: uid=admin,dc=github,dc=com
20 | objectClass: top
21 | objectClass: person
22 | objectClass: organizationalPerson
23 | objectClass: inetOrgPerson
24 | cn: system administrator
25 | sn: administrator
26 | displayName: Directory Superuser
27 | uid: admin
28 | userPassword: secret
29 |
30 | dn: uid=sean,dc=github,dc=com
31 | cn: Sean Bryant
32 | cn: Sean
33 | sn: Bryant
34 | uid: sean
35 | userPassword: secret
36 | mail: sean@github.com
37 | mail: sbryant@github.com
38 | objectClass: inetOrgPerson
39 |
40 | dn: uid=calavera,dc=github,dc=com
41 | cn: David Calavera
42 | cn: David
43 | sn: Calavera
44 | uid: calavera
45 | userPassword: passworD1
46 | mail: calavera@github.com
47 | objectClass: inetOrgPerson
48 |
49 | dn: uid=ldaptest,dc=github,dc=com
50 | cn: LDAP
51 | sn: Test
52 | uid: ldaptest
53 | userPassword: secret
54 | mail: ldaptest@github.com
55 | objectClass: inetOrgPerson
56 |
57 | dn: uid=newuserindb,dc=github,dc=com
58 | cn: LDAP
59 | sn: Test
60 | uid: newuserindb
61 | userPassword: secret
62 | mail: newuserindb@github.com
63 | objectClass: inetOrgPerson
64 |
--------------------------------------------------------------------------------
/lib/github/ldap/group.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | # This class represents an LDAP group.
4 | # It encapsulates operations that you can perform against a group, like retrieving its members.
5 | #
6 | # To get a group, you'll need to create a `Ldap` object and then call the method `group` with the name of its base.
7 | #
8 | # For example:
9 | #
10 | # domain = GitHub::Ldap.new(options).group("cn=enterprise,dc=github,dc=com")
11 | #
12 | class Group
13 | include Filter
14 |
15 | GROUP_CLASS_NAMES = %w(groupOfNames groupOfUniqueNames posixGroup group)
16 |
17 | attr_reader :ldap, :entry
18 |
19 | def initialize(ldap, entry)
20 | @ldap, @entry = ldap, entry
21 | end
22 |
23 | # Public - Get all members that belong to a group.
24 | # This list also includes the members of subgroups.
25 | #
26 | # Returns an array with all the member entries.
27 | def members
28 | return @all_members if @all_members
29 | group_and_member_entries
30 | @all_members
31 | end
32 |
33 | # Public - Get all the subgroups from a group recursively.
34 | #
35 | # Returns an array with all the subgroup entries.
36 | def subgroups
37 | return @all_groups if @all_groups
38 | group_and_member_entries
39 | @all_groups
40 | end
41 |
42 | # Public - Check if a user dn is included in the members of this group and its subgroups.
43 | #
44 | # user_entry: is the user entry to check the membership.
45 | #
46 | # Returns true if the dn is in the list of members.
47 | def is_member?(user_entry)
48 | member_names.include?(user_entry.dn) ||
49 | members.detect {|entry| entry.dn == user_entry.dn}
50 | end
51 |
52 |
53 | # Internal - Get all the member entries for a group.
54 | #
55 | # Returns an array of Net::LDAP::Entry.
56 | def member_entries
57 | @member_entries ||= member_names.each_with_object([]) do |m, a|
58 | entry = @ldap.domain(m).bind
59 | a << entry if entry
60 | end
61 | end
62 |
63 | # Internal - Get all the names under `member` and `uniqueMember`.
64 | #
65 | # Returns an array with all the DN members.
66 | def member_names
67 | MEMBERSHIP_NAMES.each_with_object([]) do |n, cache|
68 | cache.concat @entry[n]
69 | end
70 | end
71 |
72 | # Internal: Returns true if the object class(es) provided match a group's.
73 | def group?(object_class)
74 | self.class.group?(object_class)
75 | end
76 |
77 | # Internal - Check if an object class includes the member names
78 | # Use `&` rathen than `include?` because both are arrays.
79 | #
80 | # NOTE: object classes are downcased by default in Net::LDAP, so this
81 | # will fail to match correctly unless we also downcase our group classes.
82 | #
83 | # Returns true if the object class includes one of the group class names.
84 | def self.group?(object_class)
85 | !(GROUP_CLASS_NAMES.map(&:downcase) & object_class.map(&:downcase)).empty?
86 | end
87 |
88 | # Internal - Generate a hash with all the group DNs for caching purposes.
89 | #
90 | # groups: is an array of group entries.
91 | #
92 | # Returns a hash with the cache groups.
93 | def load_cache(groups)
94 | groups.each_with_object({}) {|entry, h| h[entry.dn] = true }
95 | end
96 |
97 | # Internal - Iterate over a collection of groups recursively.
98 | # Remove groups already inspected before iterating over subgroups.
99 | #
100 | # groups: is an array of group entries.
101 | # cache: is a hash where the keys are group dns.
102 | # block: is a block to call with the groups and members of subgroups.
103 | #
104 | # Returns nothing.
105 | def loop_cached_groups(groups, cache, &block)
106 | groups.each do |result|
107 | subgroups, members = @ldap.group(result.dn).groups_and_members
108 |
109 | subgroups.delete_if {|entry| cache[entry.dn]}
110 | subgroups.each {|entry| cache[entry.dn] = true}
111 |
112 | block.call(subgroups, members)
113 | loop_cached_groups(subgroups, cache, &block)
114 | end
115 | end
116 |
117 | # Internal - Divide members of a group in user and subgroups.
118 | #
119 | # Returns two arrays, the first one with subgroups and the second one with users.
120 | def groups_and_members
121 | member_entries.partition {|e| group?(e[:objectclass])}
122 | end
123 |
124 | # Internal - Inspect the ldap server searching for group and member entries.
125 | #
126 | # Returns two arrays, the first one with subgroups and the second one with users.
127 | def group_and_member_entries
128 | groups, members = groups_and_members
129 | @all_members = members
130 | @all_groups = groups
131 |
132 | cache = load_cache(groups)
133 |
134 | loop_cached_groups(groups, cache) do |subgroups, users|
135 | @all_groups.concat subgroups
136 | @all_members.concat users
137 | end
138 |
139 | @all_members.uniq! {|m| m.dn }
140 |
141 | [@all_groups, @all_members]
142 | end
143 | end
144 | end
145 | end
146 |
--------------------------------------------------------------------------------
/lib/github/ldap/instrumentation.rb:
--------------------------------------------------------------------------------
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
29 |
--------------------------------------------------------------------------------
/lib/github/ldap/member_search.rb:
--------------------------------------------------------------------------------
1 | require 'github/ldap/member_search/base'
2 | require 'github/ldap/member_search/classic'
3 | require 'github/ldap/member_search/recursive'
4 | require 'github/ldap/member_search/active_directory'
5 |
--------------------------------------------------------------------------------
/lib/github/ldap/member_search/active_directory.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | module MemberSearch
4 | # Look up group members using the ActiveDirectory "in chain" matching rule.
5 | #
6 | # The 1.2.840.113556.1.4.1941 matching rule (LDAP_MATCHING_RULE_IN_CHAIN)
7 | # "walks the chain of ancestry in objects all the way to the root until
8 | # it finds a match".
9 | # Source: http://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx
10 | #
11 | # This means we have an efficient method of searching for group members,
12 | # even in nested groups, performed on the server side.
13 | class ActiveDirectory < Base
14 | OID = "1.2.840.113556.1.4.1941"
15 |
16 | # Internal: The default attributes to query for.
17 | # NOTE: We technically don't need any by default, but if we left this
18 | # empty, we'd be querying for *all* attributes which is less ideal.
19 | DEFAULT_ATTRS = %w(objectClass)
20 |
21 | # Internal: The attributes to search for.
22 | attr_reader :attrs
23 |
24 | # Public: Instantiate new search strategy.
25 | #
26 | # - ldap: GitHub::Ldap object
27 | # - options: Hash of options
28 | #
29 | # NOTE: This overrides default behavior to configure attrs`.
30 | def initialize(ldap, options = {})
31 | super
32 | @attrs = Array(options[:attrs]).concat DEFAULT_ATTRS
33 | end
34 |
35 | # Public: Performs search for group members, including groups and
36 | # members of subgroups, using ActiveDirectory's "in chain" matching
37 | # rule.
38 | #
39 | # Returns Array of Net::LDAP::Entry objects.
40 | def perform(group)
41 | filter = member_of_in_chain_filter(group)
42 |
43 | # search for all members of the group, including subgroups, by
44 | # searching "in chain".
45 | domains.each_with_object([]) do |domain, members|
46 | members.concat domain.search(filter: filter, attributes: attrs)
47 | end
48 | end
49 |
50 | # Internal: Constructs a member filter using the "in chain"
51 | # extended matching rule afforded by ActiveDirectory.
52 | #
53 | # Returns a Net::LDAP::Filter object.
54 | def member_of_in_chain_filter(entry)
55 | Net::LDAP::Filter.ex("memberOf:#{OID}", entry.dn)
56 | end
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/github/ldap/member_search/base.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | module MemberSearch
4 | class Base
5 |
6 | # Internal: The GitHub::Ldap object to search domains with.
7 | attr_reader :ldap
8 |
9 | # Public: Instantiate new search strategy.
10 | #
11 | # - ldap: GitHub::Ldap object
12 | # - options: Hash of options
13 | def initialize(ldap, options = {})
14 | @ldap = ldap
15 | @options = options
16 | end
17 |
18 | # Public: Abstract: Performs search for group members.
19 | #
20 | # Returns Array of Net::LDAP::Entry objects.
21 | # def perform(entry)
22 | # end
23 |
24 | # Internal: Domains to search through.
25 | #
26 | # Returns an Array of GitHub::Ldap::Domain objects.
27 | def domains
28 | @domains ||= ldap.search_domains.map { |base| ldap.domain(base) }
29 | end
30 | private :domains
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/github/ldap/member_search/classic.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | module MemberSearch
4 | # Look up group members using the existing `Group#members` and
5 | # `Group#subgroups` API.
6 | class Classic < Base
7 | # Public: Performs search for group members, including groups and
8 | # members of subgroups recursively.
9 | #
10 | # Returns Array of Net::LDAP::Entry objects.
11 | def perform(group_entry)
12 | group = ldap.load_group(group_entry)
13 | group.members + group.subgroups
14 | end
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/github/ldap/member_search/recursive.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | module MemberSearch
4 | # Look up group members recursively.
5 | #
6 | # This results in a maximum of `depth` iterations/recursions to look up
7 | # members of a group and its subgroups.
8 | class Recursive < Base
9 | include Filter
10 |
11 | DEFAULT_MAX_DEPTH = 9
12 | DEFAULT_ATTRS = %w(member uniqueMember memberUid)
13 |
14 | # Internal: The maximum depth to search for members.
15 | attr_reader :depth
16 |
17 | # Internal: The attributes to search for.
18 | attr_reader :attrs
19 |
20 | # Public: Instantiate new search strategy.
21 | #
22 | # - ldap: GitHub::Ldap object
23 | # - options: Hash of options
24 | #
25 | # NOTE: This overrides default behavior to configure `depth` and `attrs`.
26 | def initialize(ldap, options = {})
27 | super
28 | @depth = options[:depth] || DEFAULT_MAX_DEPTH
29 | @attrs = Array(options[:attrs]).concat DEFAULT_ATTRS
30 | end
31 |
32 | # Public: Performs search for group members, including groups and
33 | # members of subgroups recursively.
34 | #
35 | # Returns Array of Net::LDAP::Entry objects.
36 | def perform(group)
37 | # track groups found
38 | found = Hash.new
39 |
40 | # track all DNs searched for (so we don't repeat searches)
41 | searched = Set.new
42 |
43 | # if this is a posixGroup, return members immediately (no nesting)
44 | uids = member_uids(group)
45 | return entries_by_uid(uids) if uids.any?
46 |
47 | # track group
48 | searched << group.dn
49 | found[group.dn] = group
50 |
51 | # pull out base group's member DNs
52 | dns = member_dns(group)
53 |
54 | # search for base group's subgroups
55 | groups = dns.each_with_object([]) do |dn, groups|
56 | groups.concat find_groups_by_dn(dn)
57 | searched << dn
58 | end
59 |
60 | # track found groups
61 | groups.each { |g| found[g.dn] = g }
62 |
63 | # recursively find subgroups
64 | unless groups.empty?
65 | depth.times do |n|
66 | # pull out subgroups' member DNs to search through
67 | sub_dns = groups.each_with_object([]) do |subgroup, sub_dns|
68 | sub_dns.concat member_dns(subgroup)
69 | end
70 |
71 | # filter out if already searched for
72 | sub_dns.reject! { |dn| searched.include?(dn) }
73 |
74 | # give up if there's nothing else to search for
75 | break if sub_dns.empty?
76 |
77 | # search for subgroups
78 | subgroups = sub_dns.each_with_object([]) do |dn, subgroups|
79 | subgroups.concat find_groups_by_dn(dn)
80 | searched << dn
81 | end
82 |
83 | # give up if there were no subgroups found
84 | break if subgroups.empty?
85 |
86 | # track found subgroups
87 | subgroups.each { |g| found[g.dn] = g }
88 |
89 | # descend another level
90 | groups = subgroups
91 | end
92 | end
93 |
94 | # entries to return
95 | entries = []
96 |
97 | # collect all member DNs, discarding dupes and subgroup DNs
98 | members = found.values.each_with_object([]) do |group, dns|
99 | entries << group
100 | dns.concat member_dns(group)
101 | end.uniq.reject { |dn| found.key?(dn) }
102 |
103 | # wrap member DNs in Net::LDAP::Entry objects
104 | entries.concat members.map! { |dn| Net::LDAP::Entry.new(dn) }
105 |
106 | entries
107 | end
108 |
109 | # Internal: Search for Groups by DN.
110 | #
111 | # Given a Distinguished Name (DN) String value, find the Group entry
112 | # that matches it. The DN may map to a `person` entry, but we want to
113 | # filter those out.
114 | #
115 | # This will find zero or one entry most of the time, but it's not
116 | # guaranteed so we account for the possibility of more.
117 | #
118 | # This method is intended to be used with `Array#concat` by the caller.
119 | #
120 | # Returns an Array of zero or more Net::LDAP::Entry objects.
121 | def find_groups_by_dn(dn)
122 | ldap.search \
123 | base: dn,
124 | scope: Net::LDAP::SearchScope_BaseObject,
125 | attributes: attrs,
126 | filter: ALL_GROUPS_FILTER
127 | end
128 | private :find_groups_by_dn
129 |
130 | # Internal: Fetch entries by UID.
131 | #
132 | # Returns an Array of Net::LDAP::Entry objects.
133 | def entries_by_uid(members)
134 | filter = members.map { |uid| Net::LDAP::Filter.eq(ldap.uid, uid) }.reduce(:|)
135 | domains.each_with_object([]) do |domain, entries|
136 | entries.concat domain.search(filter: filter, attributes: attrs)
137 | end.compact
138 | end
139 | private :entries_by_uid
140 |
141 | # Internal: Returns an Array of String DNs for `groupOfNames` and
142 | # `uniqueGroupOfNames` members.
143 | def member_dns(entry)
144 | MEMBERSHIP_NAMES.each_with_object([]) do |attr_name, members|
145 | members.concat entry[attr_name]
146 | end
147 | end
148 | private :member_dns
149 |
150 | # Internal: Returns an Array of String UIDs for PosixGroups members.
151 | def member_uids(entry)
152 | entry["memberUid"]
153 | end
154 | private :member_uids
155 | end
156 | end
157 | end
158 | end
159 |
--------------------------------------------------------------------------------
/lib/github/ldap/membership_validators.rb:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/lib/github/ldap/membership_validators/active_directory.rb:
--------------------------------------------------------------------------------
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 | #
28 | # Use ReferralChaser to chase any potential referrals for an entry that may be owned by a different
29 | # domain controller.
30 | matched = referral_chaser.search \
31 | filter: membership_in_chain_filter(entry),
32 | base: entry.dn,
33 | scope: Net::LDAP::SearchScope_BaseObject,
34 | return_referrals: true,
35 | attributes: ATTRS
36 |
37 | # membership validated if entry was matched and returned as a result
38 | # Active Directory DNs are case-insensitive
39 | Array(matched).map { |m| m.dn.downcase }.include?(entry.dn.downcase)
40 | end
41 |
42 | def referral_chaser
43 | @referral_chaser ||= GitHub::Ldap::ReferralChaser.new(@ldap)
44 | end
45 |
46 | # Internal: Constructs a membership filter using the "in chain"
47 | # extended matching rule afforded by ActiveDirectory.
48 | #
49 | # Returns a Net::LDAP::Filter object.
50 | def membership_in_chain_filter(entry)
51 | group_dns.map do |dn|
52 | Net::LDAP::Filter.ex("memberOf:#{OID}", dn)
53 | end.reduce(:|)
54 | end
55 |
56 | # Internal: the group DNs to check against.
57 | #
58 | # Returns an Array of String DNs.
59 | def group_dns
60 | @group_dns ||= groups.map(&:dn)
61 | end
62 | end
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/lib/github/ldap/membership_validators/base.rb:
--------------------------------------------------------------------------------
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 | # - options: Hash of options
17 | def initialize(ldap, groups, options = {})
18 | @ldap = ldap
19 | @groups = groups
20 | @options = options
21 | end
22 |
23 | # Abstract: Performs the membership validation check.
24 | #
25 | # Returns Boolean whether the entry's membership is validated or not.
26 | # def perform(entry)
27 | # end
28 |
29 | # Internal: Domains to search through.
30 | #
31 | # Returns an Array of GitHub::Ldap::Domain objects.
32 | def domains
33 | @domains ||= ldap.search_domains.map { |base| ldap.domain(base) }
34 | end
35 | private :domains
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/github/ldap/membership_validators/classic.rb:
--------------------------------------------------------------------------------
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
35 |
--------------------------------------------------------------------------------
/lib/github/ldap/membership_validators/recursive.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | module MembershipValidators
4 | # Validates membership recursively.
5 | #
6 | # The first step checks whether the entry is a direct member of the given
7 | # groups. If they are, then we've validated membership successfully.
8 | #
9 | # If not, query for all of the groups that have our groups as members,
10 | # then we check if the entry is a member of any of those.
11 | #
12 | # This is repeated until the entry is found, recursing and requesting
13 | # groups in bulk each iteration until we hit the maximum depth allowed
14 | # and have to give up.
15 | #
16 | # This results in a maximum of `depth` queries (per domain) to validate
17 | # membership in a list of groups.
18 | class Recursive < Base
19 | include Filter
20 |
21 | DEFAULT_MAX_DEPTH = 9
22 | ATTRS = %w(dn cn)
23 |
24 | # Internal: The maximum depth to search for membership.
25 | attr_reader :depth
26 |
27 | # Public: Instantiate new search strategy.
28 | #
29 | # - ldap: GitHub::Ldap object
30 | # - groups: Array of Net::LDAP::Entry group objects
31 | # - options: Hash of options
32 | # depth: Integer limit of recursion
33 | #
34 | # NOTE: This overrides default behavior to configure `depth`.
35 | def initialize(ldap, groups, options = {})
36 | super
37 | @depth = options[:depth] || DEFAULT_MAX_DEPTH
38 | end
39 |
40 | def perform(entry, depth_override = nil)
41 | if depth_override
42 | warn "DEPRECATION WARNING: Calling Recursive#perform with a second argument is deprecated."
43 | warn "Usage:"
44 | warn " strategy = GitHub::Ldap::MembershipValidators::Recursive.new \\"
45 | warn " ldap, depth: 5"
46 | warn " strategy#perform(entry)"
47 | end
48 |
49 | # short circuit validation if there are no groups to check against
50 | return true if groups.empty?
51 |
52 | domains.each do |domain|
53 | # find groups entry is an immediate member of
54 | membership = domain.search(filter: member_filter(entry), attributes: ATTRS)
55 |
56 | # success if any of these groups match the restricted auth groups
57 | return true if membership.any? { |entry| group_dns.include?(entry.dn) }
58 |
59 | # give up if the entry has no memberships to recurse
60 | next if membership.empty?
61 |
62 | # recurse to at most `depth`
63 | (depth_override || depth).times do |n|
64 | # find groups whose members include membership groups
65 | membership = domain.search(filter: membership_filter(membership), attributes: ATTRS)
66 |
67 | # success if any of these groups match the restricted auth groups
68 | return true if membership.any? { |entry| group_dns.include?(entry.dn) }
69 |
70 | # give up if there are no more membersips to recurse
71 | break if membership.empty?
72 | end
73 |
74 | # give up on this base if there are no memberships to test
75 | next if membership.empty?
76 | end
77 |
78 | false
79 | end
80 |
81 | # Internal: Construct a filter to find groups this entry is a direct
82 | # member of.
83 | #
84 | # Overloads the included `GitHub::Ldap::Filters#member_filter` method
85 | # to inject `posixGroup` handling.
86 | #
87 | # Returns a Net::LDAP::Filter object.
88 | def member_filter(entry_or_uid, uid = ldap.uid)
89 | filter = super(entry_or_uid)
90 |
91 | if ldap.posix_support_enabled?
92 | if posix_filter = posix_member_filter(entry_or_uid, uid)
93 | filter |= posix_filter
94 | end
95 | end
96 |
97 | filter
98 | end
99 |
100 | # Internal: Construct a filter to find groups whose members are the
101 | # Array of String group DNs passed in.
102 | #
103 | # Returns a String filter.
104 | def membership_filter(groups)
105 | groups.map { |entry| member_filter(entry, :cn) }.reduce(:|)
106 | end
107 |
108 | # Internal: the group DNs to check against.
109 | #
110 | # Returns an Array of String DNs.
111 | def group_dns
112 | @group_dns ||= groups.map(&:dn)
113 | end
114 | end
115 | end
116 | end
117 | end
118 |
--------------------------------------------------------------------------------
/lib/github/ldap/posix_group.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | # This class represents a POSIX group.
4 | #
5 | # To get a POSIX group, you'll need to create a `Ldap` object and then call the method `group`.
6 | # The parameter for `group` must be a dn to a group entry with `posixGroup` amongs the values for the attribute `objectClass`.
7 | #
8 | # For example:
9 | #
10 | # domain = GitHub::Ldap.new(options).group("cn=enterprise,dc=github,dc=com")
11 | #
12 | class PosixGroup < Group
13 | # Public - Check if an ldap entry is a valid posixGroup.
14 | #
15 | # entry: is the ldap entry to check.
16 | #
17 | # Returns true if the entry includes the objectClass `posixGroup`.
18 | def self.valid?(entry)
19 | entry[:objectClass].any? {|oc| oc.downcase == 'posixgroup'}
20 | end
21 |
22 | # Public - Overrides Group#members
23 | #
24 | # Search the entries corresponding to the members in the `memberUid` attribute.
25 | # It calls `super` if the group entry includes `member` or `uniqueMember`.
26 | #
27 | # Returns an array with the members of this group and its submembers if there is any.
28 | def members
29 | return @all_posix_members if @all_posix_members
30 |
31 | @all_posix_members = search_members_by_uids
32 | @all_posix_members.concat super if combined_group?
33 |
34 | @all_posix_members.uniq! {|m| m.dn }
35 | @all_posix_members
36 | end
37 |
38 | # Public - Overrides Group#subgroups
39 | #
40 | # Prevent to call super when the group entry does not include `member` or `uniqueMember`.
41 | #
42 | # Returns an array with the subgroups of this group.
43 | def subgroups
44 | return [] unless combined_group?
45 |
46 | super
47 | end
48 |
49 | # Public - Overrides Group#is_member?
50 | #
51 | # Chech if the user entry uid exists in the collection of `memberUid`.
52 | # It calls `super` if the group entry includes `member` or `uniqueMember`.
53 | #
54 | # Return true if the user is member if this group or any subgroup.
55 | def is_member?(user_entry)
56 | entry_uids = user_entry[ldap.uid]
57 | return true if !(entry_uids & entry[:memberUid]).empty?
58 |
59 | super if combined_group?
60 | end
61 |
62 | # Internal - Check if this posix group also includes `member` and `uniqueMember` entries.
63 | #
64 | # Returns true if any of the membership names is include in this group entry.
65 | def combined_group?
66 | MEMBERSHIP_NAMES.any? {|name| !entry[name].empty? }
67 | end
68 |
69 | # Internal - Search all members by uid.
70 | #
71 | # Return an array of user entries.
72 | def search_members_by_uids
73 | member_uids = entry[:memberUid]
74 | return [] if member_uids.empty?
75 |
76 | filter = all_members_by_uid(member_uids, ldap.uid)
77 | ldap.search(filter: filter)
78 | end
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/github/ldap/referral_chaser.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 |
4 | # This class adds referral chasing capability to a GitHub::Ldap connection.
5 | #
6 | # See: https://technet.microsoft.com/en-us/library/cc978014.aspx
7 | # http://www.umich.edu/~dirsvcs/ldap/doc/other/ldap-ref.html
8 | #
9 | class ReferralChaser
10 |
11 | # Public - Creates a ReferralChaser that decorates an instance of GitHub::Ldap
12 | # with additional functionality to the #search method, allowing it to chase
13 | # any referral entries and aggregate the results into a single response.
14 | #
15 | # connection - The instance of GitHub::Ldap to use for searching. Will use
16 | # the connection's authentication, (admin_user and admin_password) as credentials
17 | # for connecting to referred domain controllers.
18 | def initialize(connection)
19 | @connection = connection
20 | @admin_user = connection.admin_user
21 | @admin_password = connection.admin_password
22 | @port = connection.port
23 | end
24 |
25 | # Public - Search the domain controller represented by this instance's connection.
26 | # If a referral is returned, search only one of the domain controllers indicated
27 | # by the referral entries, per RFC 4511 (https://tools.ietf.org/html/rfc4511):
28 | #
29 | # "If the client wishes to progress the operation, it contacts one of
30 | # the supported services found in the referral. If multiple URIs are
31 | # present, the client assumes that any supported URI may be used to
32 | # progress the operation."
33 | #
34 | # options - is a hash with the same options that Net::LDAP::Connection#search supports.
35 | # Referral searches will use the given options, but will replace options[:base]
36 | # with the referral URL's base search dn.
37 | #
38 | # Does not take a block argument as GitHub::Ldap and Net::LDAP::Connection#search do.
39 | #
40 | # Will not recursively follow any subsequent referrals.
41 | #
42 | # Returns an Array of Net::LDAP::Entry.
43 | def search(options)
44 | search_results = []
45 | referral_entries = []
46 |
47 | search_results = connection.search(options) do |entry|
48 | if entry && entry[:search_referrals]
49 | referral_entries << entry
50 | end
51 | end
52 |
53 | unless referral_entries.empty?
54 | entry = referral_entries.first
55 | referral_string = entry[:search_referrals].first
56 | if GitHub::Ldap::URL.valid?(referral_string)
57 | referral = Referral.new(referral_string, admin_user, admin_password, port)
58 | search_results = referral.search(options)
59 | end
60 | end
61 |
62 | Array(search_results)
63 | end
64 |
65 | private
66 |
67 | attr_reader :connection, :admin_user, :admin_password, :port
68 |
69 | # Represents a referral entry from an LDAP search result. Constructs a corresponding
70 | # GitHub::Ldap object from the paramaters on the referral_url and provides a #search
71 | # method to continue the search on the referred domain.
72 | class Referral
73 | def initialize(referral_url, admin_user, admin_password, port=nil)
74 | url = GitHub::Ldap::URL.new(referral_url)
75 | @search_base = url.dn
76 |
77 | connection_options = {
78 | host: url.host,
79 | port: port || url.port,
80 | scope: url.scope,
81 | admin_user: admin_user,
82 | admin_password: admin_password
83 | }
84 |
85 | @connection = GitHub::Ldap::ConnectionCache.get_connection(connection_options)
86 | end
87 |
88 | # Search the referred domain controller with options, merging in the referred search
89 | # base DN onto options[:base].
90 | def search(options)
91 | connection.search(options.merge(base: search_base))
92 | end
93 |
94 | attr_reader :search_base, :connection
95 | end
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/lib/github/ldap/server.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | require 'ladle'
4 |
5 | # Preconfigured user fixtures. If you want to use them for your own tests.
6 | DEFAULT_FIXTURES_PATH = File.expand_path('fixtures.ldif', File.dirname(__FILE__))
7 |
8 | DEFAULT_SERVER_OPTIONS = {
9 | user_fixtures: DEFAULT_FIXTURES_PATH,
10 | user_domain: 'dc=github,dc=com',
11 | admin_user: 'uid=admin,dc=github,dc=com',
12 | admin_password: 'secret',
13 | quiet: true,
14 | port: 3897
15 | }
16 |
17 | class << self
18 |
19 | # server_options: is the options used to start the server,
20 | # useful to know in development.
21 | attr_reader :server_options
22 |
23 | # ldap_server: is the instance of the testing ldap server,
24 | # you should never interact with it,
25 | # but it's used to grecefully stop it after your tests finalize.
26 | attr_reader :ldap_server
27 | end
28 |
29 | # Start a testing server.
30 | # If there is already a server initialized it doesn't do anything.
31 | #
32 | # options: is a hash with the custom options for the server.
33 | def self.start_server(options = {})
34 | @server_options = DEFAULT_SERVER_OPTIONS.merge(options)
35 |
36 | @server_options[:allow_anonymous] ||= false
37 | @server_options[:ldif] = @server_options[:user_fixtures]
38 | @server_options[:domain] = @server_options[:user_domain]
39 | @server_options[:tmpdir] ||= server_tmp
40 |
41 | @server_options[:quiet] = false if @server_options[:verbose]
42 |
43 | @ldap_server = Ladle::Server.new(@server_options)
44 | @ldap_server.start
45 | end
46 |
47 | # Stop the testing server.
48 | # If there is no server started this method doesn't do anything.
49 | def self.stop_server
50 | ldap_server && ldap_server.stop
51 | end
52 |
53 | # Determine the temporal directory where the ldap server lives.
54 | # If there is no temporal directory in the environment we create one in the base path.
55 | #
56 | # Returns the path to the temporal directory.
57 | def self.server_tmp
58 | tmp = ENV['TMPDIR'] || ENV['TEMPDIR']
59 |
60 | if tmp.nil?
61 | tmp = 'tmp'
62 | Dir.mkdir(tmp) unless File.directory?('tmp')
63 | end
64 |
65 | tmp
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/github/ldap/url.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 |
4 | # This class represents an LDAP URL
5 | #
6 | # See: https://tools.ietf.org/html/rfc4516#section-2
7 | # https://docs.oracle.com/cd/E19957-01/817-6707/urls.html
8 | #
9 | class URL
10 | extend Forwardable
11 | SCOPES = {
12 | "base" => Net::LDAP::SearchScope_BaseObject,
13 | "one" => Net::LDAP::SearchScope_SingleLevel,
14 | "sub" => Net::LDAP::SearchScope_WholeSubtree
15 | }
16 | SCOPES.default = Net::LDAP::SearchScope_BaseObject
17 |
18 | attr_reader :dn, :attributes, :scope, :filter
19 |
20 | def_delegators :@uri, :port, :host, :scheme
21 |
22 | # Public - Creates a new GitHub::Ldap::URL object with :port, :host and :scheme
23 | # delegated to a URI object parsed from url_string, and then parses the
24 | # query params according to the LDAP specification.
25 | #
26 | # url_string - An LDAP URL string.
27 | # returns - a GitHub::Ldap::URL with the following attributes:
28 | # host - Name or IP of the LDAP server.
29 | # port - The given port, defaults to 389.
30 | # dn - The base search DN.
31 | # attributes - The comma-delimited list of attributes to be returned.
32 | # scope - The scope of the search.
33 | # filter - Search filter to apply to entries within the specified scope of the search.
34 | #
35 | # Supported LDAP URL strings look like this, where sections in brackets are optional:
36 | #
37 | # ldap[s]://[hostport][/[dn[?[attributes][?[scope][?[filter]]]]]]
38 | #
39 | # where:
40 | #
41 | # hostport is a host name with an optional ":portnumber"
42 | # dn is the base DN to be used for an LDAP search operation
43 | # attributes is a comma separated list of attributes to be retrieved
44 | # scope is one of these three strings: base one sub (default=base)
45 | # filter is LDAP search filter as used in a call to ldap_search
46 | #
47 | # For example:
48 | #
49 | # ldap://dc4.ghe.local:456/CN=Maggie,DC=dc4,DC=ghe,DC=local?cn,mail?base?(cn=Charlie)
50 | #
51 | def initialize(url_string)
52 | if !self.class.valid?(url_string)
53 | raise InvalidLdapURLException.new("Invalid LDAP URL: #{url_string}")
54 | end
55 | @uri = URI(url_string)
56 | @dn = URI.unescape(@uri.path.sub(/^\//, ""))
57 | if @uri.query
58 | @attributes, @scope, @filter = @uri.query.split("?")
59 | end
60 | end
61 |
62 | def self.valid?(url_string)
63 | url_string =~ URI::regexp && ["ldap", "ldaps"].include?(URI(url_string).scheme)
64 | end
65 |
66 | # Maps the returned scope value from the URL to one of Net::LDAP::Scopes
67 | #
68 | # The URL scope value can be one of:
69 | # "base" - retrieves information only about the DN (base_dn) specified.
70 | # "one" - retrieves information about entries one level below the DN (base_dn) specified. The base entry is not included in this scope.
71 | # "sub" - retrieves information about entries at all levels below the DN (base_dn) specified. The base entry is included in this scope.
72 | #
73 | # Which will map to one of the following Net::LDAP::Scopes:
74 | # SearchScope_BaseObject = 0
75 | # SearchScope_SingleLevel = 1
76 | # SearchScope_WholeSubtree = 2
77 | #
78 | # If no scope or an invalid scope is given, defaults to SearchScope_BaseObject
79 | def net_ldap_scope
80 | Net::LDAP::SearchScopes[SCOPES[scope]]
81 | end
82 |
83 | class InvalidLdapURLException < Exception; end
84 | end
85 | end
86 | end
87 |
88 |
--------------------------------------------------------------------------------
/lib/github/ldap/user_search/active_directory.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | module UserSearch
4 | class ActiveDirectory < Default
5 |
6 | private
7 |
8 | # Private - Overridden from base class to set the base to "", and use the
9 | # Global Catalog to perform the user search.
10 | def search(search_options)
11 | Array(global_catalog_connection.search(search_options.merge(options)))
12 | end
13 |
14 | def global_catalog_connection
15 | GlobalCatalog.connection(ldap)
16 | end
17 |
18 | # When doing a global search for a user's DN, set the search base to blank
19 | def options
20 | super.merge(base: "")
21 | end
22 | end
23 |
24 | class GlobalCatalog < Net::LDAP
25 | STANDARD_GC_PORT = 3268
26 | LDAPS_GC_PORT = 3269
27 |
28 | # Returns a connection to the Active Directory Global Catalog
29 | #
30 | # See: https://technet.microsoft.com/en-us/library/cc728188(v=ws.10).aspx
31 | #
32 | def self.connection(ldap)
33 | @global_catalog_instance ||= begin
34 | netldap = ldap.connection
35 | # This is ugly, but Net::LDAP doesn't expose encryption or auth
36 | encryption = netldap.instance_variable_get(:@encryption)
37 | auth = netldap.instance_variable_get(:@auth)
38 |
39 | new({
40 | host: ldap.host,
41 | instrumentation_service: ldap.instrumentation_service,
42 | port: encryption ? LDAPS_GC_PORT : STANDARD_GC_PORT,
43 | auth: auth,
44 | encryption: encryption
45 | })
46 | end
47 | end
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/github/ldap/user_search/default.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | module UserSearch
4 | # The default user search strategy, mainly for allowing Domain#user? to
5 | # search for a user on the configured domain controller, or use the Global
6 | # Catalog to search across the entire Active Directory forest.
7 | class Default
8 | include Filter
9 |
10 | def initialize(ldap)
11 | @ldap = ldap
12 | @options = {
13 | :attributes => [],
14 | :paged_searches_supported => true,
15 | :size => 1
16 | }
17 | end
18 |
19 | # Performs a normal search on the configured domain controller
20 | # using the default base DN, uid, search_options
21 | def perform(login, base_name, uid, search_options)
22 | search_options[:filter] = login_filter(uid, login)
23 | search_options[:base] = base_name
24 | search(options.merge(search_options))
25 | end
26 |
27 | # The default search. This can be overridden by a child class
28 | # like GitHub::Ldap::UserSearch::ActiveDirectory to change the
29 | # scope of the search.
30 | def search(options)
31 | ldap.search(options)
32 | end
33 |
34 | private
35 |
36 | attr_reader :options, :ldap
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/github/ldap/virtual_attributes.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | class VirtualAttributes
4 | def initialize(enabled, attributes = {})
5 | @enabled = enabled
6 | @attributes = attributes
7 | end
8 |
9 | def enabled?
10 | @enabled
11 | end
12 |
13 | def virtual_membership
14 | @attributes.fetch(:virtual_membership, "memberOf")
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/github/ldap/virtual_group.rb:
--------------------------------------------------------------------------------
1 | module GitHub
2 | class Ldap
3 | class VirtualGroup < Group
4 | include Filter
5 |
6 | def members
7 | @ldap.search(filter: members_of_group(@entry.dn, membership_attribute))
8 | end
9 |
10 | def subgroups
11 | @ldap.search(filter: subgroups_of_group(@entry.dn, membership_attribute))
12 | end
13 |
14 | def is_member(user_dn)
15 | @ldap.search(filter: is_member_of_group(user_dn, @entry.dn, membership_attribute))
16 | end
17 |
18 | # Internal - Get the attribute to use for membership filtering.
19 | #
20 | # Returns a string.
21 | def membership_attribute
22 | @ldap.virtual_attributes.virtual_membership
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/script/changelog:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | # Usage: script/changelog [-r ] [-b ] [-h ]
3 | #
4 | # repo: base string of GitHub repository url. e.g. "user_or_org/repository". Defaults to git remote url.
5 | # base: git ref to compare from. e.g. "v1.3.1". Defaults to latest git tag.
6 | # head: git ref to compare to. Defaults to "HEAD".
7 | #
8 | # Generate a changelog preview from pull requests merged between `base` and
9 | # `head`.
10 | #
11 | set -e
12 |
13 | [ $# -eq 0 ] && set -- --help
14 |
15 | # parse args
16 | repo=$(git remote -v | grep push | awk '{print $2}' | cut -d'/' -f4- | sed 's/\.git//')
17 | base=$(git tag -l | sort -n | tail -n 1)
18 | head="HEAD"
19 | api_url="https://api.github.com"
20 |
21 | echo "# $repo $base..$head"
22 | echo
23 |
24 | # get merged PR's. Better way is to query the API for these, but this is easier
25 | for pr in $(git log --oneline $base..$head | grep "Merge pull request" | awk '{gsub("#",""); print $5}')
26 | do
27 | # frustrated with trying to pull out the right values, fell back to ruby
28 | curl -s "$api_url/repos/$repo/pulls/$pr" | ruby -rjson -e 'pr=JSON.parse(STDIN.read); puts "* #{pr[%q(title)]} [##{pr[%q(number)]}](#{pr[%q(html_url)]})"'
29 | done
30 |
--------------------------------------------------------------------------------
/script/cibuild-apacheds:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | set -e
3 | set -x
4 |
5 | cd `dirname $0`/..
6 |
7 | bundle exec rake
8 |
--------------------------------------------------------------------------------
/script/cibuild-openldap:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | set -e
3 | set -x
4 |
5 | cd `dirname $0`/..
6 |
7 | bundle exec rake
8 |
--------------------------------------------------------------------------------
/script/install-openldap:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | set -e
3 | set -x
4 |
5 | BASE_PATH="$( cd `dirname $0`/../test/fixtures/openldap && pwd )"
6 | SEED_PATH="$( cd `dirname $0`/../test/fixtures/common && pwd )"
7 |
8 | DEBIAN_FRONTEND=noninteractive sudo -E apt-get install -y --force-yes slapd time ldap-utils
9 |
10 | sudo /etc/init.d/slapd stop
11 |
12 | TMPDIR=$(mktemp -d)
13 | cd $TMPDIR
14 |
15 | # Delete data and reconfigure.
16 | sudo rm -rf /etc/ldap/slapd.d/*
17 | sudo rm -rf /var/lib/ldap/*
18 | sudo slapadd -F /etc/ldap/slapd.d -b "cn=config" -l $BASE_PATH/slapd.conf.ldif
19 | # Load memberof and ref-int overlays and configure them.
20 | sudo slapadd -F /etc/ldap/slapd.d -b "cn=config" -l $BASE_PATH/memberof.ldif
21 |
22 | # Add base domain.
23 | sudo slapadd -F /etc/ldap/slapd.d < "ad1.ghe.dev"))
7 | conn2 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad1.ghe.dev"))
8 | assert_equal conn1.object_id, conn2.object_id
9 | end
10 |
11 | def test_creates_new_connections_per_host
12 | conn1 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad1.ghe.dev"))
13 | conn2 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad2.ghe.dev"))
14 | conn3 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad2.ghe.dev"))
15 | refute_equal conn1.object_id, conn2.object_id
16 | assert_equal conn2.object_id, conn3.object_id
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/domain_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | module GitHubLdapDomainTestCases
4 | def setup
5 | @ldap = GitHub::Ldap.new(options)
6 | @domain = @ldap.domain("dc=github,dc=com")
7 | end
8 |
9 | def test_user_valid_login
10 | assert user = @domain.valid_login?('user1', 'passworD1')
11 | assert_equal 'uid=user1,ou=People,dc=github,dc=com', user.dn
12 | end
13 |
14 | def test_user_with_invalid_password
15 | assert !@domain.valid_login?('user1', 'foo'),
16 | "Login `user1` expected to be invalid with password `foo`"
17 | end
18 |
19 | def test_user_with_invalid_login
20 | assert !@domain.valid_login?('bar', 'foo'),
21 | "Login `bar` expected to be invalid with password `foo`"
22 | end
23 |
24 | def test_groups_in_server
25 | assert_equal 2, @domain.groups(%w(ghe-users ghe-admins)).size
26 | end
27 |
28 | def test_user_in_group
29 | assert user = @domain.valid_login?('user1', 'passworD1')
30 |
31 | assert @domain.is_member?(user, %w(ghe-users ghe-admins)),
32 | "Expected `ghe-users` or `ghe-admins` to include the member `#{user.dn}`"
33 | end
34 |
35 | def test_user_not_in_different_group
36 | user = @domain.valid_login?('user1', 'passworD1')
37 |
38 | refute @domain.is_member?(user, %w(ghe-admins)),
39 | "Expected `ghe-admins` not to include the member `#{user.dn}`"
40 | end
41 |
42 | def test_user_without_group
43 | user = @domain.valid_login?('groupless-user1', 'passworD1')
44 |
45 | assert !@domain.is_member?(user, %w(all-users)),
46 | "Expected `all-users` not to include the member `#{user.dn}`"
47 | end
48 |
49 | def test_authenticate_returns_valid_users
50 | user = @domain.authenticate!('user1', 'passworD1')
51 | assert_equal 'uid=user1,ou=People,dc=github,dc=com', user.dn
52 | end
53 |
54 | def test_authenticate_doesnt_return_invalid_users
55 | refute @domain.authenticate!('user1', 'foo'),
56 | "Expected `authenticate!` to not return an invalid user"
57 | end
58 |
59 | def test_authenticate_check_valid_user_and_groups
60 | user = @domain.authenticate!('user1', 'passworD1', %w(ghe-users ghe-admins))
61 |
62 | assert_equal 'uid=user1,ou=People,dc=github,dc=com', user.dn
63 | end
64 |
65 | def test_authenticate_doesnt_return_valid_users_in_different_groups
66 | refute @domain.authenticate!('user1', 'passworD1', %w(ghe-admins)),
67 | "Expected `authenticate!` to not return an user"
68 | end
69 |
70 | def test_membership_empty_for_non_members
71 | user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind
72 |
73 | assert @domain.membership(user, %w(ghe-admins)).empty?,
74 | "Expected `user1` not to be a member of `ghe-admins`."
75 | end
76 |
77 | def test_membership_groups_for_members
78 | user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind
79 | groups = @domain.membership(user, %w(ghe-users ghe-admins))
80 |
81 | assert_equal 1, groups.size
82 | assert_equal 'cn=ghe-users,ou=Groups,dc=github,dc=com', groups.first.dn
83 | end
84 |
85 | def test_membership_with_virtual_attributes
86 | ldap = GitHub::Ldap.new(options.merge(virtual_attributes: true))
87 |
88 | user = ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind
89 | user[:memberof] = 'cn=ghe-admins,ou=Groups,dc=github,dc=com'
90 |
91 | domain = ldap.domain("dc=github,dc=com")
92 | groups = domain.membership(user, %w(ghe-admins))
93 |
94 | assert_equal 1, groups.size
95 | assert_equal 'cn=ghe-admins,ou=Groups,dc=github,dc=com', groups.first.dn
96 | end
97 |
98 | def test_search
99 | assert 1, @domain.search(
100 | attributes: %w(uid),
101 | filter: Net::LDAP::Filter.eq('uid', 'user1')).size
102 | end
103 |
104 | def test_search_override_base_name
105 | assert 1, @domain.search(
106 | base: "this base name is incorrect",
107 | attributes: %w(uid),
108 | filter: Net::LDAP::Filter.eq('uid', 'user1')).size
109 | end
110 |
111 | def test_user_exists
112 | assert user = @domain.user?('user1')
113 | assert_equal 'uid=user1,ou=People,dc=github,dc=com', user.dn
114 | end
115 |
116 | def test_user_wildcards_are_filtered
117 | refute @domain.user?('user*'), 'Expected uid `user*` to not complete'
118 | end
119 |
120 | def test_user_does_not_exist
121 | refute @domain.user?('foobar'), 'Expected uid `foobar` to not exist.'
122 | end
123 |
124 | def test_user_returns_every_attribute
125 | assert user = @domain.user?('user1')
126 | assert_equal ['user1@github.com'], user[:mail]
127 | end
128 |
129 | def test_user_returns_subset_of_attributes
130 | assert entry = @domain.user?('user1', :attributes => [:cn])
131 | assert_equal [:dn, :cn], entry.attribute_names
132 | end
133 |
134 | def test_auth_binds
135 | assert user = @domain.user?('user1')
136 | assert @domain.auth(user, 'passworD1'), 'Expected user to bind'
137 | end
138 |
139 | def test_auth_does_not_bind
140 | assert user = @domain.user?('user1')
141 | refute @domain.auth(user, 'foo'), 'Expected user not not bind'
142 | end
143 |
144 | def test_user_search_returns_first_entry
145 | entry = mock("Net::Ldap::Entry")
146 | search_strategy = mock("GitHub::Ldap::UserSearch::Default")
147 | search_strategy.stubs(:perform).returns([entry])
148 | @ldap.expects(:user_search_strategy).returns(search_strategy)
149 | user = @domain.user?('user1', :attributes => [:cn])
150 | assert_equal entry, user
151 | end
152 | end
153 |
154 | class GitHubLdapDomainTest < GitHub::Ldap::Test
155 | include GitHubLdapDomainTestCases
156 | end
157 |
158 | class GitHubLdapDomainUnauthenticatedTest < GitHub::Ldap::UnauthenticatedTest
159 | include GitHubLdapDomainTestCases
160 | end
161 |
162 | class GitHubLdapDomainNestedGroupsTest < GitHub::Ldap::Test
163 | def setup
164 | @ldap = GitHub::Ldap.new(options)
165 | @domain = @ldap.domain("dc=github,dc=com")
166 | end
167 |
168 | def test_membership_in_subgroups
169 | user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind
170 |
171 | assert @domain.is_member?(user, %w(nested-groups)),
172 | "Expected `nested-groups` to include the member `#{user.dn}`"
173 | end
174 |
175 | def test_membership_in_deeply_nested_subgroups
176 | assert user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind
177 |
178 | assert @domain.is_member?(user, %w(n-depth-nested-group4)),
179 | "Expected `n-depth-nested-group4` to include the member `#{user.dn}` via deep recursion"
180 | end
181 | end
182 |
183 | class GitHubLdapPosixGroupsWithRecursionFallbackTest < GitHub::Ldap::Test
184 | def setup
185 | opts = options.merge \
186 | recursive_group_search_fallback: true
187 | @ldap = GitHub::Ldap.new(opts)
188 | @domain = @ldap.domain("dc=github,dc=com")
189 | @cn = "posix-group1"
190 | end
191 |
192 | def test_membership_for_posixGroups
193 | assert user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind
194 |
195 | assert @domain.is_member?(user, [@cn]),
196 | "Expected `#{@cn}` to include the member `#{user.dn}`"
197 | end
198 | end
199 |
200 | class GitHubLdapPosixGroupsWithoutRecursionTest < GitHub::Ldap::Test
201 | def setup
202 | opts = options.merge \
203 | recursive_group_search_fallback: false
204 | @ldap = GitHub::Ldap.new(opts)
205 | @domain = @ldap.domain("dc=github,dc=com")
206 | @cn = "posix-group1"
207 | end
208 |
209 | def test_membership_for_posixGroups
210 | assert user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind
211 |
212 | assert @domain.is_member?(user, [@cn]),
213 | "Expected `#{@cn}` to include the member `#{user.dn}`"
214 | end
215 | end
216 |
217 | # Specifically testing that this doesn't break when posixGroups are not
218 | # supported.
219 | class GitHubLdapWithoutPosixGroupsTest < GitHub::Ldap::Test
220 | def setup
221 | opts = options.merge \
222 | recursive_group_search_fallback: false, # test non-recursive group membership search
223 | posix_support: false # disable posixGroup support
224 | @ldap = GitHub::Ldap.new(opts)
225 | @domain = @ldap.domain("dc=github,dc=com")
226 | @cn = "posix-group1"
227 | end
228 |
229 | def test_membership_for_posixGroups
230 | assert user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind
231 |
232 | refute @domain.is_member?(user, [@cn]),
233 | "Expected `#{@cn}` to not include the member `#{user.dn}`"
234 | end
235 | end
236 |
237 | class GitHubLdapActiveDirectoryGroupsTest < GitHub::Ldap::Test
238 | def run(*)
239 | return super if self.class.test_env == "activedirectory"
240 | Minitest::Result.from(self)
241 | end
242 |
243 | def test_filter_groups
244 | domain = GitHub::Ldap.new(options).domain("DC=ad,DC=ghe,DC=local")
245 | results = domain.filter_groups("ghe-admins")
246 | assert_equal 1, results.size
247 | end
248 | end
249 |
--------------------------------------------------------------------------------
/test/filter_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | class FilterTest < GitHub::Ldap::Test
4 | class Subject
5 | include GitHub::Ldap::Filter
6 | def initialize(ldap)
7 | @ldap = ldap
8 | end
9 | end
10 |
11 | # Fake a Net::LDAP::Entry
12 | class Entry < Struct.new(:dn, :uid)
13 | def [](field)
14 | Array(send(field))
15 | end
16 | end
17 |
18 | def setup
19 | @ldap = GitHub::Ldap.new(options.merge(:uid => 'uid'))
20 | @subject = Subject.new(@ldap)
21 | @me = 'uid=calavera,dc=github,dc=com'
22 | @uid = "calavera"
23 | @entry = Net::LDAP::Entry.new(@me)
24 | @entry[:uid] = @uid
25 | end
26 |
27 | def test_member_present
28 | assert_equal "(|(member=*)(uniqueMember=*))", @subject.member_filter.to_s
29 | end
30 |
31 | def test_member_equal
32 | assert_equal "(|(member=#{@me})(uniqueMember=#{@me}))",
33 | @subject.member_filter(@entry).to_s
34 | end
35 |
36 | def test_member_equal_with_string
37 | assert_equal "(|(member=#{@me})(uniqueMember=#{@me}))",
38 | @subject.member_filter(@entry.dn).to_s
39 | end
40 |
41 | def test_posix_member_without_uid
42 | @entry.uid = nil
43 | assert_nil @subject.posix_member_filter(@entry, @ldap.uid)
44 | end
45 |
46 | def test_posix_member_equal
47 | assert_equal "(memberUid=#{@uid})",
48 | @subject.posix_member_filter(@entry, @ldap.uid).to_s
49 | end
50 |
51 | def test_posix_member_equal_string
52 | assert_equal "(memberUid=#{@uid})",
53 | @subject.posix_member_filter(@uid).to_s
54 | end
55 |
56 | def test_groups_reduced
57 | assert_equal "(|(cn=Enterprise)(cn=People))",
58 | @subject.group_filter(%w(Enterprise People)).to_s
59 | end
60 |
61 | def test_members_of_group
62 | assert_equal "(memberOf=cn=group,dc=github,dc=com)",
63 | @subject.members_of_group('cn=group,dc=github,dc=com').to_s
64 |
65 | assert_equal "(isMemberOf=cn=group,dc=github,dc=com)",
66 | @subject.members_of_group('cn=group,dc=github,dc=com', 'isMemberOf').to_s
67 | end
68 |
69 | def test_subgroups_of_group
70 | assert_equal "(&(memberOf=cn=group,dc=github,dc=com)#{Subject::ALL_GROUPS_FILTER})",
71 | @subject.subgroups_of_group('cn=group,dc=github,dc=com').to_s
72 |
73 | assert_equal "(&(isMemberOf=cn=group,dc=github,dc=com)#{Subject::ALL_GROUPS_FILTER})",
74 | @subject.subgroups_of_group('cn=group,dc=github,dc=com', 'isMemberOf').to_s
75 | end
76 |
77 | def test_all_members_by_uid
78 | assert_equal "(|(uid=calavera)(uid=mtodd))",
79 | @subject.all_members_by_uid(%w(calavera mtodd), :uid).to_s
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/test/fixtures/common/seed.ldif:
--------------------------------------------------------------------------------
1 | dn: ou=People,dc=github,dc=com
2 | objectClass: top
3 | objectClass: organizationalUnit
4 | ou: People
5 |
6 | dn: ou=Groups,dc=github,dc=com
7 | objectClass: top
8 | objectClass: organizationalUnit
9 | ou: Groups
10 |
11 | # Directory Superuser
12 | dn: uid=admin,dc=github,dc=com
13 | uid: admin
14 | cn: system administrator
15 | sn: administrator
16 | objectClass: top
17 | objectClass: person
18 | objectClass: organizationalPerson
19 | objectClass: inetOrgPerson
20 | displayName: Directory Superuser
21 | userPassword: passworD1
22 |
23 | # Users 1-10
24 |
25 | dn: uid=user1,ou=People,dc=github,dc=com
26 | uid: user1
27 | cn: user1
28 | sn: user1
29 | objectClass: top
30 | objectClass: person
31 | objectClass: organizationalPerson
32 | objectClass: inetOrgPerson
33 | userPassword: passworD1
34 | mail: user1@github.com
35 |
36 | dn: uid=user2,ou=People,dc=github,dc=com
37 | uid: user2
38 | cn: user2
39 | sn: user2
40 | objectClass: top
41 | objectClass: person
42 | objectClass: organizationalPerson
43 | objectClass: inetOrgPerson
44 | userPassword: passworD1
45 | mail: user2@github.com
46 |
47 | dn: uid=user3,ou=People,dc=github,dc=com
48 | uid: user3
49 | cn: user3
50 | sn: user3
51 | objectClass: top
52 | objectClass: person
53 | objectClass: organizationalPerson
54 | objectClass: inetOrgPerson
55 | userPassword: passworD1
56 | mail: user3@github.com
57 |
58 | dn: uid=user4,ou=People,dc=github,dc=com
59 | uid: user4
60 | cn: user4
61 | sn: user4
62 | objectClass: top
63 | objectClass: person
64 | objectClass: organizationalPerson
65 | objectClass: inetOrgPerson
66 | userPassword: passworD1
67 | mail: user4@github.com
68 |
69 | dn: uid=user5,ou=People,dc=github,dc=com
70 | uid: user5
71 | cn: user5
72 | sn: user5
73 | objectClass: top
74 | objectClass: person
75 | objectClass: organizationalPerson
76 | objectClass: inetOrgPerson
77 | userPassword: passworD1
78 | mail: user5@github.com
79 |
80 | dn: uid=user6,ou=People,dc=github,dc=com
81 | uid: user6
82 | cn: user6
83 | sn: user6
84 | objectClass: top
85 | objectClass: person
86 | objectClass: organizationalPerson
87 | objectClass: inetOrgPerson
88 | userPassword: passworD1
89 | mail: user6@github.com
90 |
91 | dn: uid=user7,ou=People,dc=github,dc=com
92 | uid: user7
93 | cn: user7
94 | sn: user7
95 | objectClass: top
96 | objectClass: person
97 | objectClass: organizationalPerson
98 | objectClass: inetOrgPerson
99 | userPassword: passworD1
100 | mail: user7@github.com
101 |
102 | dn: uid=user8,ou=People,dc=github,dc=com
103 | uid: user8
104 | cn: user8
105 | sn: user8
106 | objectClass: top
107 | objectClass: person
108 | objectClass: organizationalPerson
109 | objectClass: inetOrgPerson
110 | userPassword: passworD1
111 | mail: user8@github.com
112 |
113 | dn: uid=user9,ou=People,dc=github,dc=com
114 | uid: user9
115 | cn: user9
116 | sn: user9
117 | objectClass: top
118 | objectClass: person
119 | objectClass: organizationalPerson
120 | objectClass: inetOrgPerson
121 | userPassword: passworD1
122 | mail: user9@github.com
123 |
124 | dn: uid=user10,ou=People,dc=github,dc=com
125 | uid: user10
126 | cn: user10
127 | sn: user10
128 | objectClass: top
129 | objectClass: person
130 | objectClass: organizationalPerson
131 | objectClass: inetOrgPerson
132 | userPassword: passworD1
133 | mail: user10@github.com
134 |
135 | # Emailless User
136 |
137 | dn: uid=emailless-user1,ou=People,dc=github,dc=com
138 | uid: emailless-user1
139 | cn: emailless-user1
140 | sn: emailless-user1
141 | objectClass: top
142 | objectClass: person
143 | objectClass: organizationalPerson
144 | objectClass: inetOrgPerson
145 | userPassword: passworD1
146 |
147 | # Groupless User
148 |
149 | dn: uid=groupless-user1,ou=People,dc=github,dc=com
150 | uid: groupless-user1
151 | cn: groupless-user1
152 | sn: groupless-user1
153 | objectClass: top
154 | objectClass: person
155 | objectClass: organizationalPerson
156 | objectClass: inetOrgPerson
157 | userPassword: passworD1
158 |
159 | # Admin User
160 |
161 | dn: uid=admin1,ou=People,dc=github,dc=com
162 | uid: admin1
163 | cn: admin1
164 | sn: admin1
165 | objectClass: top
166 | objectClass: person
167 | objectClass: organizationalPerson
168 | objectClass: inetOrgPerson
169 | userPassword: passworD1
170 | mail: admin1@github.com
171 |
172 | # Groups
173 |
174 | dn: cn=ghe-users,ou=Groups,dc=github,dc=com
175 | cn: ghe-users
176 | objectClass: groupOfNames
177 | member: uid=user1,ou=People,dc=github,dc=com
178 | member: uid=emailless-user1,ou=People,dc=github,dc=com
179 |
180 | dn: cn=all-users,ou=Groups,dc=github,dc=com
181 | cn: all-users
182 | objectClass: groupOfNames
183 | member: cn=ghe-users,ou=Groups,dc=github,dc=com
184 | member: uid=user1,ou=People,dc=github,dc=com
185 | member: uid=user2,ou=People,dc=github,dc=com
186 | member: uid=user3,ou=People,dc=github,dc=com
187 | member: uid=user4,ou=People,dc=github,dc=com
188 | member: uid=user5,ou=People,dc=github,dc=com
189 | member: uid=user6,ou=People,dc=github,dc=com
190 | member: uid=user7,ou=People,dc=github,dc=com
191 | member: uid=user8,ou=People,dc=github,dc=com
192 | member: uid=user9,ou=People,dc=github,dc=com
193 | member: uid=user10,ou=People,dc=github,dc=com
194 | member: uid=emailless-user1,ou=People,dc=github,dc=com
195 |
196 | dn: cn=ghe-admins,ou=Groups,dc=github,dc=com
197 | cn: ghe-admins
198 | objectClass: groupOfNames
199 | member: uid=admin1,ou=People,dc=github,dc=com
200 |
201 | dn: cn=all-admins,ou=Groups,dc=github,dc=com
202 | cn: all-admins
203 | objectClass: groupOfNames
204 | member: cn=ghe-admins,ou=Groups,dc=github,dc=com
205 | member: uid=admin1,ou=People,dc=github,dc=com
206 |
207 | dn: cn=n-member-group10,ou=Groups,dc=github,dc=com
208 | cn: n-member-group10
209 | objectClass: groupOfNames
210 | member: uid=user1,ou=People,dc=github,dc=com
211 | member: uid=user2,ou=People,dc=github,dc=com
212 | member: uid=user3,ou=People,dc=github,dc=com
213 | member: uid=user4,ou=People,dc=github,dc=com
214 | member: uid=user5,ou=People,dc=github,dc=com
215 | member: uid=user6,ou=People,dc=github,dc=com
216 | member: uid=user7,ou=People,dc=github,dc=com
217 | member: uid=user8,ou=People,dc=github,dc=com
218 | member: uid=user9,ou=People,dc=github,dc=com
219 | member: uid=user10,ou=People,dc=github,dc=com
220 |
221 | dn: cn=nested-group1,ou=Groups,dc=github,dc=com
222 | cn: nested-group1
223 | objectClass: groupOfNames
224 | member: uid=user1,ou=People,dc=github,dc=com
225 | member: uid=user2,ou=People,dc=github,dc=com
226 | member: uid=user3,ou=People,dc=github,dc=com
227 | member: uid=user4,ou=People,dc=github,dc=com
228 | member: uid=user5,ou=People,dc=github,dc=com
229 | member: uid=user6,ou=People,dc=github,dc=com
230 | member: uid=user7,ou=People,dc=github,dc=com
231 | member: uid=user8,ou=People,dc=github,dc=com
232 | member: uid=user9,ou=People,dc=github,dc=com
233 | member: uid=user10,ou=People,dc=github,dc=com
234 |
235 | dn: cn=nested-groups,ou=Groups,dc=github,dc=com
236 | cn: nested-groups
237 | objectClass: groupOfNames
238 | member: cn=nested-group1,ou=Groups,dc=github,dc=com
239 |
240 | dn: cn=n-member-nested-group1,ou=Groups,dc=github,dc=com
241 | cn: n-member-nested-group1
242 | objectClass: groupOfNames
243 | member: cn=nested-group1,ou=Groups,dc=github,dc=com
244 |
245 | dn: cn=deeply-nested-group0.0.0,ou=Groups,dc=github,dc=com
246 | cn: deeply-nested-group0.0.0
247 | objectClass: groupOfNames
248 | member: uid=user1,ou=People,dc=github,dc=com
249 | member: uid=user2,ou=People,dc=github,dc=com
250 | member: uid=user3,ou=People,dc=github,dc=com
251 | member: uid=user4,ou=People,dc=github,dc=com
252 | member: uid=user5,ou=People,dc=github,dc=com
253 |
254 | dn: cn=deeply-nested-group0.0.1,ou=Groups,dc=github,dc=com
255 | cn: deeply-nested-group0.0.1
256 | objectClass: groupOfNames
257 | member: uid=user6,ou=People,dc=github,dc=com
258 | member: uid=user7,ou=People,dc=github,dc=com
259 | member: uid=user8,ou=People,dc=github,dc=com
260 | member: uid=user9,ou=People,dc=github,dc=com
261 | member: uid=user10,ou=People,dc=github,dc=com
262 |
263 | dn: cn=deeply-nested-group0.0,ou=Groups,dc=github,dc=com
264 | cn: deeply-nested-group0.0
265 | objectClass: groupOfNames
266 | member: cn=deeply-nested-group0.0.0,ou=Groups,dc=github,dc=com
267 | member: cn=deeply-nested-group0.0.1,ou=Groups,dc=github,dc=com
268 |
269 | dn: cn=deeply-nested-group0,ou=Groups,dc=github,dc=com
270 | cn: deeply-nested-group0
271 | objectClass: groupOfNames
272 | member: cn=deeply-nested-group0.0,ou=Groups,dc=github,dc=com
273 |
274 | dn: cn=deeply-nested-groups,ou=Groups,dc=github,dc=com
275 | cn: deeply-nested-groups
276 | objectClass: groupOfNames
277 | member: cn=deeply-nested-group0,ou=Groups,dc=github,dc=com
278 |
279 | dn: cn=n-depth-nested-group1,ou=Groups,dc=github,dc=com
280 | cn: n-depth-nested-group1
281 | objectClass: groupOfNames
282 | member: cn=nested-group1,ou=Groups,dc=github,dc=com
283 |
284 | dn: cn=n-depth-nested-group2,ou=Groups,dc=github,dc=com
285 | cn: n-depth-nested-group2
286 | objectClass: groupOfNames
287 | member: cn=n-depth-nested-group1,ou=Groups,dc=github,dc=com
288 |
289 | dn: cn=n-depth-nested-group3,ou=Groups,dc=github,dc=com
290 | cn: n-depth-nested-group3
291 | objectClass: groupOfNames
292 | member: cn=n-depth-nested-group2,ou=Groups,dc=github,dc=com
293 |
294 | dn: cn=n-depth-nested-group4,ou=Groups,dc=github,dc=com
295 | cn: n-depth-nested-group4
296 | objectClass: groupOfNames
297 | member: cn=n-depth-nested-group3,ou=Groups,dc=github,dc=com
298 |
299 | dn: cn=n-depth-nested-group5,ou=Groups,dc=github,dc=com
300 | cn: n-depth-nested-group5
301 | objectClass: groupOfNames
302 | member: cn=n-depth-nested-group4,ou=Groups,dc=github,dc=com
303 |
304 | dn: cn=n-depth-nested-group6,ou=Groups,dc=github,dc=com
305 | cn: n-depth-nested-group6
306 | objectClass: groupOfNames
307 | member: cn=n-depth-nested-group5,ou=Groups,dc=github,dc=com
308 |
309 | dn: cn=n-depth-nested-group7,ou=Groups,dc=github,dc=com
310 | cn: n-depth-nested-group7
311 | objectClass: groupOfNames
312 | member: cn=n-depth-nested-group6,ou=Groups,dc=github,dc=com
313 |
314 | dn: cn=n-depth-nested-group8,ou=Groups,dc=github,dc=com
315 | cn: n-depth-nested-group8
316 | objectClass: groupOfNames
317 | member: cn=n-depth-nested-group7,ou=Groups,dc=github,dc=com
318 |
319 | dn: cn=n-depth-nested-group9,ou=Groups,dc=github,dc=com
320 | cn: n-depth-nested-group9
321 | objectClass: groupOfNames
322 | member: cn=n-depth-nested-group8,ou=Groups,dc=github,dc=com
323 |
324 | dn: cn=head-group,ou=Groups,dc=github,dc=com
325 | cn: head-group
326 | objectClass: groupOfNames
327 | member: cn=tail-group,ou=Groups,dc=github,dc=com
328 | member: uid=user1,ou=People,dc=github,dc=com
329 | member: uid=user2,ou=People,dc=github,dc=com
330 | member: uid=user3,ou=People,dc=github,dc=com
331 | member: uid=user4,ou=People,dc=github,dc=com
332 | member: uid=user5,ou=People,dc=github,dc=com
333 |
334 | dn: cn=tail-group,ou=Groups,dc=github,dc=com
335 | cn: tail-group
336 | objectClass: groupOfNames
337 | member: cn=head-group,ou=Groups,dc=github,dc=com
338 | member: uid=user6,ou=People,dc=github,dc=com
339 | member: uid=user7,ou=People,dc=github,dc=com
340 | member: uid=user8,ou=People,dc=github,dc=com
341 | member: uid=user9,ou=People,dc=github,dc=com
342 | member: uid=user10,ou=People,dc=github,dc=com
343 |
344 | dn: cn=recursively-nested-groups,ou=Groups,dc=github,dc=com
345 | cn: recursively-nested-groups
346 | objectClass: groupOfNames
347 | member: cn=head-group,ou=Groups,dc=github,dc=com
348 | member: cn=tail-group,ou=Groups,dc=github,dc=com
349 |
350 | # posixGroup
351 |
352 | dn: cn=posix-group1,ou=Groups,dc=github,dc=com
353 | cn: posix-group1
354 | objectClass: posixGroup
355 | gidNumber: 1001
356 | memberUid: user1
357 | memberUid: user2
358 | memberUid: user3
359 | memberUid: user4
360 | memberUid: user5
361 |
362 | # missing members
363 |
364 | dn: cn=missing-users,ou=Groups,dc=github,dc=com
365 | cn: missing-users
366 | objectClass: groupOfNames
367 | member: uid=user1,ou=People,dc=github,dc=com
368 | member: uid=user2,ou=People,dc=github,dc=com
369 | member: uid=nonexistent-user,ou=People,dc=github,dc=com
370 |
--------------------------------------------------------------------------------
/test/fixtures/openldap/memberof.ldif:
--------------------------------------------------------------------------------
1 | dn: cn=module,cn=config
2 | cn: module
3 | objectClass: olcModuleList
4 | objectClass: top
5 | olcModulePath: /usr/lib/ldap
6 | olcModuleLoad: memberof.la
7 |
8 | dn: olcOverlay={0}memberof,olcDatabase={1}hdb,cn=config
9 | objectClass: olcConfig
10 | objectClass: olcMemberOf
11 | objectClass: olcOverlayConfig
12 | objectClass: top
13 | olcOverlay: memberof
14 | olcMemberOfDangling: ignore
15 | olcMemberOfRefInt: TRUE
16 | olcMemberOfGroupOC: groupOfNames
17 | olcMemberOfMemberAD: member
18 | olcMemberOfMemberOfAD: memberOf
19 |
20 | dn: cn=module,cn=config
21 | cn: module
22 | objectclass: olcModuleList
23 | objectclass: top
24 | olcmoduleload: refint.la
25 | olcmodulepath: /usr/lib/ldap
26 |
27 | dn: olcOverlay={1}refint,olcDatabase={1}hdb,cn=config
28 | objectClass: olcConfig
29 | objectClass: olcOverlayConfig
30 | objectClass: olcRefintConfig
31 | objectClass: top
32 | olcOverlay: {1}refint
33 | olcRefintAttribute: memberof member manager owner
34 |
--------------------------------------------------------------------------------
/test/fixtures/openldap/slapd.conf.ldif:
--------------------------------------------------------------------------------
1 | dn: cn=config
2 | objectClass: olcGlobal
3 | cn: config
4 | olcPidFile: /var/run/slapd/slapd.pid
5 | olcArgsFile: /var/run/slapd/slapd.args
6 | olcLogLevel: none
7 | olcToolThreads: 1
8 |
9 | dn: olcDatabase={-1}frontend,cn=config
10 | objectClass: olcDatabaseConfig
11 | objectClass: olcFrontendConfig
12 | olcDatabase: {-1}frontend
13 | olcSizeLimit: 500
14 | olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
15 | olcAccess: {1}to dn.exact="" by * read
16 | olcAccess: {2}to dn.base="cn=Subschema" by * read
17 |
18 | dn: olcDatabase=config,cn=config
19 | objectClass: olcDatabaseConfig
20 | olcDatabase: config
21 | olcAccess: to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
22 |
23 | dn: cn=schema,cn=config
24 | objectClass: olcSchemaConfig
25 | cn: schema
26 |
27 | include: file:///etc/ldap/schema/core.ldif
28 | include: file:///etc/ldap/schema/cosine.ldif
29 | include: file:///etc/ldap/schema/nis.ldif
30 | include: file:///etc/ldap/schema/inetorgperson.ldif
31 |
32 | dn: cn=module{0},cn=config
33 | objectClass: olcModuleList
34 | cn: module{0}
35 | olcModulePath: /usr/lib/ldap
36 | olcModuleLoad: back_hdb
37 |
38 | dn: olcBackend=hdb,cn=config
39 | objectClass: olcBackendConfig
40 | olcBackend: hdb
41 |
42 | dn: olcDatabase=hdb,cn=config
43 | objectClass: olcDatabaseConfig
44 | objectClass: olcHdbConfig
45 | olcDatabase: hdb
46 | olcDbCheckpoint: 512 30
47 | olcDbConfig: set_cachesize 1 0 0
48 | olcDbConfig: set_lk_max_objects 1500
49 | olcDbConfig: set_lk_max_locks 1500
50 | olcDbConfig: set_lk_max_lockers 1500
51 | olcLastMod: TRUE
52 | olcSuffix: dc=github,dc=com
53 | olcDbDirectory: /var/lib/ldap
54 | olcRootDN: cn=admin,dc=github,dc=com
55 | # admin's password: "passworD1"
56 | olcRootPW: {SHA}LFSkM9eegU6j3PeGG7UuHrT/KZM=
57 | olcDbIndex: objectClass eq
58 | olcAccess: to attrs=userPassword,shadowLastChange
59 | by self write
60 | by anonymous auth
61 | by dn="cn=admin,dc=github,dc=com" write
62 | by * none
63 | olcAccess: to dn.base="" by * read
64 | olcAccess: to *
65 | by self write
66 | by dn="cn=admin,dc=github,dc=com" write
67 | by * read
68 |
--------------------------------------------------------------------------------
/test/fixtures/posixGroup.schema.ldif:
--------------------------------------------------------------------------------
1 | version: 1
2 |
3 | # attributetype ( 1.3.6.1.1.1.1.1 NAME 'gidNumber'
4 | # DESC 'An integer uniquely identifying a group in an administrative domain'
5 | # EQUALITY integerMatch
6 | # SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
7 | dn: m-oid=1.3.6.1.1.1.1.1,ou=attributeTypes,cn=other,ou=schema
8 | objectClass: metaAttributeType
9 | objectClass: metaTop
10 | objectClass: top
11 | m-collective: FALSE
12 | m-description: An integer uniquely identifying a group in an administrative domain
13 | m-equality: integerMatch
14 | m-name: gidNumber
15 | m-syntax: 1.3.6.1.4.1.1466.115.121.1.27
16 | m-usage: USER_APPLICATIONS
17 | m-oid: 1.3.6.1.1.1.1.1
18 |
19 | # attributetype ( 1.3.6.1.1.1.1.12 NAME 'memberUid'
20 | # EQUALITY caseExactIA5Match
21 | # SUBSTR caseExactIA5SubstringsMatch
22 | # SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
23 | dn: m-oid=1.3.6.1.1.1.1.12,ou=attributeTypes,cn=other,ou=schema
24 | objectClass: metaAttributeType
25 | objectClass: metaTop
26 | objectClass: top
27 | m-collective: FALSE
28 | m-description: memberUid
29 | m-equality: caseExactIA5Match
30 | m-name: memberUid
31 | m-syntax: 1.3.6.1.4.1.1466.115.121.1.26
32 | m-usage: USER_APPLICATIONS
33 | m-oid: 1.3.6.1.1.1.1.12
34 |
35 | # objectclass ( 1.3.6.1.1.1.2.2 NAME 'posixGroup' SUP top STRUCTURAL
36 | # DESC 'Abstraction of a group of accounts'
37 | # MUST ( cn $ gidNumber )
38 | # MAY ( userPassword $ memberUid $ description ) )
39 | dn: m-oid=1.3.6.1.1.1.2.2,ou=objectClasses,cn=other,ou=schema
40 | objectClass: metaObjectClass
41 | objectClass: metaTop
42 | objectClass: top
43 | m-description: posixGroup
44 | m-must: cn
45 | m-must: gidNumber
46 | m-may: memberUid
47 | m-may: userPassword
48 | m-may: description
49 | m-supobjectclass: top
50 | m-name: posixGroup
51 | m-oid: 1.3.6.1.1.1.2.2
52 | m-typeobjectclass: STRUCTURAL
53 |
--------------------------------------------------------------------------------
/test/group_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | class GitHubLdapGroupTest < GitHub::Ldap::Test
4 | def groups_domain
5 | @ldap.domain("ou=Groups,dc=github,dc=com")
6 | end
7 |
8 | def setup
9 | @ldap = GitHub::Ldap.new(options)
10 | @group = @ldap.group("cn=ghe-users,ou=Groups,dc=github,dc=com")
11 | end
12 |
13 | def test_group?
14 | assert @group.group?(%w(group))
15 | assert @group.group?(%w(groupOfUniqueNames))
16 | assert @group.group?(%w(posixGroup))
17 |
18 | object_classes = %w(groupOfNames)
19 | assert @group.group?(object_classes)
20 | assert @group.group?(object_classes.map(&:downcase))
21 | end
22 |
23 | def test_subgroups
24 | group = @ldap.group("cn=deeply-nested-group0.0,ou=Groups,dc=github,dc=com")
25 | assert_equal 2, group.subgroups.size
26 | end
27 |
28 | def test_members_from_subgroups
29 | group = @ldap.group("cn=deeply-nested-group0.0,ou=Groups,dc=github,dc=com")
30 | assert_equal 10, group.members.size
31 | end
32 |
33 | def test_all_domain_groups
34 | groups = groups_domain.all_groups
35 | assert_equal 27, groups.size
36 | end
37 |
38 | def test_filter_domain_groups
39 | groups = groups_domain.filter_groups('ghe-users')
40 | assert_equal 1, groups.size
41 | end
42 |
43 | def test_filter_domain_groups_limited
44 | groups = []
45 | groups_domain.filter_groups('deeply-nested-group', size: 1) do |entry|
46 | groups << entry
47 | end
48 | assert_equal 1, groups.size
49 | end
50 |
51 | def test_filter_domain_groups_unlimited
52 | groups = groups_domain.filter_groups('deeply-nested-group')
53 | assert_equal 5, groups.size
54 | end
55 |
56 | def test_unknown_group
57 | refute @ldap.group("cn=foobar,ou=groups,dc=github,dc=com"),
58 | "Expected to not bind any group"
59 | end
60 | end
61 |
62 | class GitHubLdapLoopedGroupTest < GitHub::Ldap::Test
63 | def setup
64 | @group = GitHub::Ldap.new(options).group("cn=recursively-nested-groups,ou=Groups,dc=github,dc=com")
65 | end
66 |
67 | def test_members_from_subgroups
68 | assert_equal 10, @group.members.size
69 | end
70 | end
71 |
72 | class GitHubLdapMissingEntriesTest < GitHub::Ldap::Test
73 | def setup
74 | @ldap = GitHub::Ldap.new(options)
75 | end
76 |
77 | def test_load_right_members
78 | assert_equal 3, @ldap.domain("cn=missing-users,ou=groups,dc=github,dc=com").bind[:member].size
79 | end
80 |
81 | def test_ignore_missing_member_entries
82 | assert_equal 2, @ldap.group("cn=missing-users,ou=groups,dc=github,dc=com").members.size
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/test/ldap_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | module GitHubLdapTestCases
4 | def setup
5 | @ldap = GitHub::Ldap.new(options)
6 | end
7 |
8 | def test_connection_with_default_options
9 | assert @ldap.test_connection, "Ldap connection expected to succeed"
10 | end
11 |
12 | def test_connection_with_list_of_hosts_with_one_valid_host
13 | ldap = GitHub::Ldap.new(options.merge(hosts: [["localhost", options[:port]]]))
14 | assert ldap.test_connection, "Ldap connection expected to succeed"
15 | end
16 |
17 | def test_connection_with_list_of_hosts_with_first_valid
18 | ldap = GitHub::Ldap.new(options.merge(hosts: [["localhost", options[:port]], ["invalid.local", options[:port]]]))
19 | assert ldap.test_connection, "Ldap connection expected to succeed"
20 | end
21 |
22 | def test_connection_with_list_of_hosts_with_first_invalid
23 | ldap = GitHub::Ldap.new(options.merge(hosts: [["invalid.local", options[:port]], ["localhost", options[:port]]]))
24 | assert ldap.test_connection, "Ldap connection expected to succeed"
25 | end
26 |
27 | def test_simple_tls
28 | expected = { method: :simple_tls, tls_options: { } }
29 | assert_equal expected, @ldap.check_encryption(:ssl)
30 | assert_equal expected, @ldap.check_encryption('SSL')
31 | assert_equal expected, @ldap.check_encryption(:simple_tls)
32 | end
33 |
34 | def test_start_tls
35 | expected = { method: :start_tls, tls_options: { } }
36 | assert_equal expected, @ldap.check_encryption(:tls)
37 | assert_equal expected, @ldap.check_encryption('TLS')
38 | assert_equal expected, @ldap.check_encryption(:start_tls)
39 | end
40 |
41 | def test_tls_validation
42 | assert_equal({ method: :start_tls, tls_options: { verify_mode: OpenSSL::SSL::VERIFY_PEER } },
43 | @ldap.check_encryption(:tls, verify_mode: OpenSSL::SSL::VERIFY_PEER))
44 | assert_equal({ method: :start_tls, tls_options: { verify_mode: OpenSSL::SSL::VERIFY_NONE } },
45 | @ldap.check_encryption(:tls, verify_mode: OpenSSL::SSL::VERIFY_NONE))
46 | assert_equal({ method: :start_tls, tls_options: { cert_store: "some/path" } },
47 | @ldap.check_encryption(:tls, cert_store: "some/path"))
48 | assert_equal({ method: :start_tls, tls_options: {} },
49 | @ldap.check_encryption(:tls, nil))
50 | end
51 |
52 | def test_search_delegator
53 | assert user = @ldap.domain('dc=github,dc=com').valid_login?('user1', 'passworD1')
54 |
55 | result = @ldap.search \
56 | :base => 'dc=github,dc=com',
57 | :attributes => %w(uid),
58 | :filter => Net::LDAP::Filter.eq('uid', 'user1')
59 |
60 | refute result.empty?
61 | assert_equal 'user1', result.first[:uid].first
62 | end
63 |
64 | def test_virtual_attributes_disabled
65 | refute @ldap.virtual_attributes.enabled?, "Expected to have virtual attributes disabled"
66 | end
67 |
68 | def test_virtual_attributes_configured
69 | ldap = GitHub::Ldap.new(options.merge(virtual_attributes: true))
70 |
71 | assert ldap.virtual_attributes.enabled?,
72 | "Expected virtual attributes to be enabled"
73 | assert_equal 'memberOf', ldap.virtual_attributes.virtual_membership
74 | end
75 |
76 | def test_virtual_attributes_configured_with_membership_attribute
77 | ldap = GitHub::Ldap.new(options.merge(virtual_attributes: {virtual_membership: "isMemberOf"}))
78 |
79 | assert ldap.virtual_attributes.enabled?,
80 | "Expected virtual attributes to be enabled"
81 | assert_equal 'isMemberOf', ldap.virtual_attributes.virtual_membership
82 | end
83 |
84 | def test_search_domains
85 | ldap = GitHub::Ldap.new(options.merge(search_domains: ['dc=github,dc=com']))
86 | result = ldap.search(filter: Net::LDAP::Filter.eq('uid', 'user1'))
87 |
88 | refute result.empty?
89 | assert_equal 'user1', result.first[:uid].first
90 | end
91 |
92 | def test_instruments_search
93 | events = @service.subscribe "search.github_ldap"
94 | result = @ldap.search(filter: "(uid=user1)", :base => "dc=github,dc=com")
95 | refute_predicate result, :empty?
96 | payload, event_result = events.pop
97 | assert payload
98 | assert event_result
99 | assert_equal result, event_result
100 | assert_equal "(uid=user1)", payload[:filter].to_s
101 | assert_equal "dc=github,dc=com", payload[:base]
102 | end
103 |
104 | def test_search_strategy_defaults
105 | assert_equal GitHub::Ldap::MembershipValidators::Recursive, @ldap.membership_validator
106 | assert_equal GitHub::Ldap::MemberSearch::Recursive, @ldap.member_search_strategy
107 | end
108 |
109 | def test_search_strategy_detects_active_directory
110 | caps = Net::LDAP::Entry.new
111 | caps[:supportedcapabilities] = [GitHub::Ldap::ACTIVE_DIRECTORY_V51_OID]
112 |
113 | @ldap.stub :capabilities, caps do
114 | @ldap.configure_search_strategy :detect
115 |
116 | assert_equal GitHub::Ldap::MembershipValidators::ActiveDirectory, @ldap.membership_validator
117 | assert_equal GitHub::Ldap::MemberSearch::ActiveDirectory, @ldap.member_search_strategy
118 | end
119 | end
120 |
121 | def test_search_strategy_configured_to_classic
122 | @ldap.configure_search_strategy :classic
123 | assert_equal GitHub::Ldap::MembershipValidators::Classic, @ldap.membership_validator
124 | assert_equal GitHub::Ldap::MemberSearch::Classic, @ldap.member_search_strategy
125 | end
126 |
127 | def test_search_strategy_configured_to_recursive
128 | @ldap.configure_search_strategy :recursive
129 | assert_equal GitHub::Ldap::MembershipValidators::Recursive, @ldap.membership_validator
130 | assert_equal GitHub::Ldap::MemberSearch::Recursive, @ldap.member_search_strategy
131 | end
132 |
133 | def test_search_strategy_configured_to_active_directory
134 | @ldap.configure_search_strategy :active_directory
135 | assert_equal GitHub::Ldap::MembershipValidators::ActiveDirectory, @ldap.membership_validator
136 | assert_equal GitHub::Ldap::MemberSearch::ActiveDirectory, @ldap.member_search_strategy
137 | end
138 |
139 | def test_search_strategy_misconfigured_to_unrecognized_strategy_falls_back_to_default
140 | @ldap.configure_search_strategy :unknown
141 | assert_equal GitHub::Ldap::MembershipValidators::Recursive, @ldap.membership_validator
142 | assert_equal GitHub::Ldap::MemberSearch::Recursive, @ldap.member_search_strategy
143 | end
144 |
145 | def test_user_search_strategy_global_catalog_when_configured
146 | @ldap.configure_user_search_strategy("global_catalog")
147 | assert_kind_of GitHub::Ldap::UserSearch::ActiveDirectory, @ldap.user_search_strategy
148 | end
149 |
150 | def test_user_search_strategy_default_when_configured
151 | @ldap.configure_user_search_strategy("default")
152 | refute_kind_of GitHub::Ldap::UserSearch::ActiveDirectory, @ldap.user_search_strategy
153 | assert_kind_of GitHub::Ldap::UserSearch::Default, @ldap.user_search_strategy
154 | end
155 |
156 | def test_capabilities
157 | assert_kind_of Net::LDAP::Entry, @ldap.capabilities
158 | end
159 | end
160 |
161 | class GitHubLdapTest < GitHub::Ldap::Test
162 | include GitHubLdapTestCases
163 | end
164 |
165 | class GitHubLdapUnauthenticatedTest < GitHub::Ldap::UnauthenticatedTest
166 | include GitHubLdapTestCases
167 | end
168 |
--------------------------------------------------------------------------------
/test/member_search/active_directory_test.rb:
--------------------------------------------------------------------------------
1 | require_relative '../test_helper'
2 |
3 | class GitHubLdapActiveDirectoryMemberSearchStubbedTest < GitHub::Ldap::Test
4 | # Only run when AD integration tests aren't run
5 | def run(*)
6 | return super if self.class.test_env != "activedirectory"
7 | Minitest::Result.from(self)
8 | end
9 |
10 | def find_group(cn)
11 | @domain.groups([cn]).first
12 | end
13 |
14 | def setup
15 | @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com)))
16 | @domain = @ldap.domain("dc=github,dc=com")
17 | @entry = @domain.user?('user1')
18 | @strategy = GitHub::Ldap::MemberSearch::ActiveDirectory.new(@ldap)
19 | end
20 |
21 | def test_finds_group_members
22 | members =
23 | @ldap.stub :search, [@entry] do
24 | @strategy.perform(find_group("nested-group1")).map(&:dn)
25 | end
26 | assert_includes members, @entry.dn
27 | end
28 |
29 | def test_finds_nested_group_members
30 | members =
31 | @ldap.stub :search, [@entry] do
32 | @strategy.perform(find_group("n-depth-nested-group1")).map(&:dn)
33 | end
34 | assert_includes members, @entry.dn
35 | end
36 |
37 | def test_finds_deeply_nested_group_members
38 | members =
39 | @ldap.stub :search, [@entry] do
40 | @strategy.perform(find_group("n-depth-nested-group9")).map(&:dn)
41 | end
42 | assert_includes members, @entry.dn
43 | end
44 | end
45 |
46 | # See test/support/vm/activedirectory/README.md for details
47 | class GitHubLdapActiveDirectoryMemberSearchIntegrationTest < GitHub::Ldap::Test
48 | # Only run this test suite if ActiveDirectory is configured
49 | def run(*)
50 | return super if self.class.test_env == "activedirectory"
51 | Minitest::Result.from(self)
52 | end
53 |
54 | def find_group(cn)
55 | @domain.groups([cn]).first
56 | end
57 |
58 | def setup
59 | @ldap = GitHub::Ldap.new(options)
60 | @domain = @ldap.domain(options[:search_domains])
61 | @entry = @domain.user?('user1')
62 | @strategy = GitHub::Ldap::MemberSearch::ActiveDirectory.new(@ldap)
63 | end
64 |
65 | def test_finds_group_members
66 | members = @strategy.perform(find_group("nested-group1")).map(&:dn)
67 | assert_includes members, @entry.dn
68 | end
69 |
70 | def test_finds_nested_group_members
71 | members = @strategy.perform(find_group("n-depth-nested-group1")).map(&:dn)
72 | assert_includes members, @entry.dn
73 | end
74 |
75 | def test_finds_deeply_nested_group_members
76 | members = @strategy.perform(find_group("n-depth-nested-group9")).map(&:dn)
77 | assert_includes members, @entry.dn
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/test/member_search/classic_test.rb:
--------------------------------------------------------------------------------
1 | require_relative '../test_helper'
2 |
3 | class GitHubLdapRecursiveMemberSearchTest < GitHub::Ldap::Test
4 | def setup
5 | @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com)))
6 | @domain = @ldap.domain("dc=github,dc=com")
7 | @entry = @domain.user?('user1')
8 | @strategy = GitHub::Ldap::MemberSearch::Classic.new(@ldap)
9 | end
10 |
11 | def find_group(cn)
12 | @domain.groups([cn]).first
13 | end
14 |
15 | def test_finds_group_members
16 | members = @strategy.perform(find_group("nested-group1")).map(&:dn)
17 | assert_includes members, @entry.dn
18 | end
19 |
20 | def test_finds_nested_group_members
21 | members = @strategy.perform(find_group("n-depth-nested-group1")).map(&:dn)
22 | assert_includes members, @entry.dn
23 | end
24 |
25 | def test_finds_deeply_nested_group_members
26 | members = @strategy.perform(find_group("n-depth-nested-group9")).map(&:dn)
27 | assert_includes members, @entry.dn
28 | end
29 |
30 | def test_finds_posix_group_members
31 | members = @strategy.perform(find_group("posix-group1")).map(&:dn)
32 | assert_includes members, @entry.dn
33 | end
34 |
35 | def test_does_not_respect_configured_depth_limit
36 | strategy = GitHub::Ldap::MemberSearch::Classic.new(@ldap, depth: 2)
37 | members = strategy.perform(find_group("n-depth-nested-group9")).map(&:dn)
38 | assert_includes members, @entry.dn
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/test/member_search/recursive_test.rb:
--------------------------------------------------------------------------------
1 | require_relative '../test_helper'
2 |
3 | class GitHubLdapRecursiveMemberSearchTest < GitHub::Ldap::Test
4 | def setup
5 | @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com)))
6 | @domain = @ldap.domain("dc=github,dc=com")
7 | @entry = @domain.user?('user1')
8 | @strategy = GitHub::Ldap::MemberSearch::Recursive.new(@ldap)
9 | end
10 |
11 | def find_group(cn)
12 | @domain.groups([cn]).first
13 | end
14 |
15 | def test_finds_group_members
16 | members = @strategy.perform(find_group("nested-group1")).map(&:dn)
17 | assert_includes members, @entry.dn
18 | end
19 |
20 | def test_finds_nested_group_members
21 | members = @strategy.perform(find_group("n-depth-nested-group1")).map(&:dn)
22 | assert_includes members, @entry.dn
23 | end
24 |
25 | def test_finds_deeply_nested_group_members
26 | members = @strategy.perform(find_group("n-depth-nested-group9")).map(&:dn)
27 | assert_includes members, @entry.dn
28 | end
29 |
30 | def test_finds_posix_group_members
31 | members = @strategy.perform(find_group("posix-group1")).map(&:dn)
32 | assert_includes members, @entry.dn
33 | end
34 |
35 | def test_respects_configured_depth_limit
36 | strategy = GitHub::Ldap::MemberSearch::Recursive.new(@ldap, depth: 2)
37 | members = strategy.perform(find_group("n-depth-nested-group9")).map(&:dn)
38 | refute_includes members, @entry.dn
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/test/membership_validators/active_directory_test.rb:
--------------------------------------------------------------------------------
1 | require_relative '../test_helper'
2 |
3 | class GitHubLdapActiveDirectoryMembershipValidatorsStubbedTest < GitHub::Ldap::Test
4 | # Only run when AD integration tests aren't run
5 | def run(*)
6 | return super if self.class.test_env != "activedirectory"
7 | Minitest::Result.from(self)
8 | end
9 |
10 | def setup
11 | @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com)))
12 | @domain = @ldap.domain("dc=github,dc=com")
13 | @entry = @domain.user?('user1')
14 | @validator = GitHub::Ldap::MembershipValidators::ActiveDirectory
15 | end
16 |
17 | def make_validator(groups)
18 | groups = @domain.groups(groups)
19 | @validator.new(@ldap, groups)
20 | end
21 |
22 | def test_validates_user_in_group
23 | validator = make_validator(%w(nested-group1))
24 |
25 | @ldap.stub :search, [@entry] do
26 | assert validator.perform(@entry)
27 | end
28 | end
29 |
30 | def test_validates_user_in_child_group
31 | validator = make_validator(%w(n-depth-nested-group1))
32 |
33 | @ldap.stub :search, [@entry] do
34 | assert validator.perform(@entry)
35 | end
36 | end
37 |
38 | def test_validates_user_in_grandchild_group
39 | validator = make_validator(%w(n-depth-nested-group2))
40 |
41 | @ldap.stub :search, [@entry] do
42 | assert validator.perform(@entry)
43 | end
44 | end
45 |
46 | def test_validates_user_in_great_grandchild_group
47 | validator = make_validator(%w(n-depth-nested-group3))
48 |
49 | @ldap.stub :search, [@entry] do
50 | assert validator.perform(@entry)
51 | end
52 | end
53 |
54 | def test_does_not_validate_user_not_in_group
55 | validator = make_validator(%w(ghe-admins))
56 |
57 | @ldap.stub :search, [] do
58 | refute validator.perform(@entry)
59 | end
60 | end
61 |
62 | def test_does_not_validate_user_not_in_any_group
63 | entry = @domain.user?('groupless-user1')
64 | validator = make_validator(%w(all-users))
65 |
66 | @ldap.stub :search, [] do
67 | refute validator.perform(entry)
68 | end
69 | end
70 | end
71 |
72 | # See test/support/vm/activedirectory/README.md for details
73 | class GitHubLdapActiveDirectoryMembershipValidatorsIntegrationTest < GitHub::Ldap::Test
74 | # Only run this test suite if ActiveDirectory is configured
75 | def run(*)
76 | return super if self.class.test_env == "activedirectory"
77 | Minitest::Result.from(self)
78 | end
79 |
80 | def setup
81 | @ldap = GitHub::Ldap.new(options)
82 | @domain = @ldap.domain(options[:search_domains])
83 | @entry = @domain.user?('user1')
84 | @validator = GitHub::Ldap::MembershipValidators::ActiveDirectory
85 | end
86 |
87 | def make_validator(groups)
88 | groups = @domain.groups(groups)
89 | @validator.new(@ldap, groups)
90 | end
91 |
92 | def test_validates_user_in_group
93 | validator = make_validator(%w(nested-group1))
94 | assert validator.perform(@entry)
95 | end
96 |
97 | def test_validates_user_in_child_group
98 | validator = make_validator(%w(n-depth-nested-group1))
99 | assert validator.perform(@entry)
100 | end
101 |
102 | def test_validates_user_in_grandchild_group
103 | validator = make_validator(%w(n-depth-nested-group2))
104 | assert validator.perform(@entry)
105 | end
106 |
107 | def test_validates_user_in_great_grandchild_group
108 | validator = make_validator(%w(n-depth-nested-group3))
109 | assert validator.perform(@entry)
110 | end
111 |
112 | def test_does_not_validate_user_not_in_group
113 | validator = make_validator(%w(ghe-admins))
114 | refute validator.perform(@entry)
115 | end
116 |
117 | def test_does_not_validate_user_not_in_any_group
118 | skip "update AD ldif to have a groupless user"
119 | @entry = @domain.user?('groupless-user1')
120 | validator = make_validator(%w(all-users))
121 | refute validator.perform(@entry)
122 | end
123 |
124 | def test_validates_user_in_posix_group
125 | validator = make_validator(%w(posix-group1))
126 | assert validator.perform(@entry)
127 | end
128 |
129 | def test_validates_user_in_group_with_differently_cased_dn
130 | validator = make_validator(%w(all-users))
131 | @entry[:dn].map(&:upcase!)
132 | assert validator.perform(@entry)
133 |
134 | @entry[:dn].map(&:downcase!)
135 | assert validator.perform(@entry)
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/test/membership_validators/classic_test.rb:
--------------------------------------------------------------------------------
1 | require_relative '../test_helper'
2 |
3 | class GitHubLdapClassicMembershipValidatorsTest < GitHub::Ldap::Test
4 | def setup
5 | @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com)))
6 | @domain = @ldap.domain("dc=github,dc=com")
7 | @entry = @domain.user?('user1')
8 | @validator = GitHub::Ldap::MembershipValidators::Classic
9 | end
10 |
11 | def make_validator(groups)
12 | groups = @domain.groups(groups)
13 | @validator.new(@ldap, groups)
14 | end
15 |
16 | def test_validates_user_in_group
17 | validator = make_validator(%w(nested-group1))
18 | assert validator.perform(@entry)
19 | end
20 |
21 | def test_validates_user_in_child_group
22 | validator = make_validator(%w(n-depth-nested-group1))
23 | assert validator.perform(@entry)
24 | end
25 |
26 | def test_validates_user_in_grandchild_group
27 | validator = make_validator(%w(n-depth-nested-group2))
28 | assert validator.perform(@entry)
29 | end
30 |
31 | def test_validates_user_in_great_grandchild_group
32 | validator = make_validator(%w(n-depth-nested-group3))
33 | assert validator.perform(@entry)
34 | end
35 |
36 | def test_does_not_validate_user_not_in_group
37 | validator = make_validator(%w(ghe-admins))
38 | refute validator.perform(@entry)
39 | end
40 |
41 | def test_does_not_validate_user_not_in_any_group
42 | @entry = @domain.user?('groupless-user1')
43 | validator = make_validator(%w(all-users))
44 | refute validator.perform(@entry)
45 | end
46 |
47 | def test_validates_user_in_posix_group
48 | validator = make_validator(%w(posix-group1))
49 | assert validator.perform(@entry)
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/test/membership_validators/recursive_test.rb:
--------------------------------------------------------------------------------
1 | require_relative '../test_helper'
2 |
3 | class GitHubLdapRecursiveMembershipValidatorsTest < GitHub::Ldap::Test
4 | def setup
5 | @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com)))
6 | @domain = @ldap.domain("dc=github,dc=com")
7 | @entry = @domain.user?('user1')
8 | @validator = GitHub::Ldap::MembershipValidators::Recursive
9 | end
10 |
11 | def make_validator(groups, options = {})
12 | groups = @domain.groups(groups)
13 | @validator.new(@ldap, groups, options)
14 | end
15 |
16 | def test_validates_user_in_group
17 | validator = make_validator(%w(nested-group1))
18 | assert validator.perform(@entry)
19 | end
20 |
21 | def test_validates_user_in_child_group
22 | validator = make_validator(%w(n-depth-nested-group1))
23 | assert validator.perform(@entry)
24 | end
25 |
26 | def test_validates_user_in_grandchild_group
27 | validator = make_validator(%w(n-depth-nested-group2))
28 | assert validator.perform(@entry)
29 | end
30 |
31 | def test_validates_user_in_great_grandchild_group
32 | validator = make_validator(%w(n-depth-nested-group3))
33 | assert validator.perform(@entry)
34 | end
35 |
36 | def test_does_not_validate_user_in_great_granchild_group_with_depth
37 | validator = make_validator(%w(n-depth-nested-group3), depth: 2)
38 | refute validator.perform(@entry)
39 | end
40 |
41 | def test_does_not_validate_user_not_in_group
42 | validator = make_validator(%w(ghe-admins))
43 | refute validator.perform(@entry)
44 | end
45 |
46 | def test_does_not_validate_user_not_in_any_group
47 | @entry = @domain.user?('groupless-user1')
48 | validator = make_validator(%w(all-users))
49 | refute validator.perform(@entry)
50 | end
51 |
52 | def test_validates_user_in_posix_group
53 | validator = make_validator(%w(posix-group1))
54 | assert validator.perform(@entry)
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/test/posix_group_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | class GitHubLdapPosixGroupTest < GitHub::Ldap::Test
4 | def setup
5 | @simple_group = Net::LDAP::Entry._load("""
6 | dn: cn=simple-group,ou=Groups,dc=github,dc=com
7 | cn: simple-group
8 | objectClass: posixGroup
9 | memberUid: user1
10 | memberUid: user2""")
11 |
12 | @one_level_deep_group = Net::LDAP::Entry._load("""
13 | dn: cn=one-level-deep-group,ou=Groups,dc=github,dc=com
14 | cn: one-level-deep-group
15 | objectClass: posixGroup
16 | objectClass: groupOfNames
17 | memberUid: user6
18 | member: cn=ghe-users,ou=Groups,dc=github,dc=com""")
19 |
20 | @two_levels_deep_group = Net::LDAP::Entry._load("""
21 | dn: cn=two-levels-deep-group,ou=Groups,dc=github,dc=com
22 | cn: two-levels-deep-group
23 | objectClass: posixGroup
24 | objectClass: groupOfNames
25 | memberUid: user6
26 | member: cn=n-depth-nested-group2,ou=Groups,dc=github,dc=com
27 | member: cn=posix-group1,ou=Groups,dc=github,dc=com""")
28 |
29 | @empty_group = Net::LDAP::Entry._load("""
30 | dn: cn=empty-group,ou=Groups,dc=github,dc=com
31 | cn: empty-group
32 | objectClass: posixGroup""")
33 |
34 | @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com)))
35 | end
36 |
37 | def test_posix_group
38 | entry = @ldap.search(filter: "(cn=posix-group1)").first
39 | assert GitHub::Ldap::PosixGroup.valid?(entry),
40 | "Expected entry to be a valid posixGroup"
41 | end
42 |
43 | def test_posix_simple_members
44 | assert group = @ldap.group("cn=posix-group1,ou=Groups,dc=github,dc=com")
45 | members = group.members
46 |
47 | assert_equal 5, members.size
48 | assert_equal %w(user1 user2 user3 user4 user5), members.map(&:uid).flatten.sort
49 | end
50 |
51 | def test_posix_combined_group
52 | group = GitHub::Ldap::PosixGroup.new(@ldap, @one_level_deep_group)
53 | members = group.members
54 |
55 | assert_equal 3, members.size
56 | end
57 |
58 | def test_posix_combined_group_unique_members
59 | group = GitHub::Ldap::PosixGroup.new(@ldap, @two_levels_deep_group)
60 | members = group.members
61 |
62 | assert_equal 10, members.size
63 | end
64 |
65 | def test_empty_subgroups
66 | group = GitHub::Ldap::PosixGroup.new(@ldap, @simple_group)
67 | subgroups = group.subgroups
68 |
69 | assert subgroups.empty?, "Simple posixgroup expected to not have subgroups"
70 | end
71 |
72 | def test_posix_combined_group_subgroups
73 | group = GitHub::Ldap::PosixGroup.new(@ldap, @one_level_deep_group)
74 | subgroups = group.subgroups
75 |
76 | assert_equal 1, subgroups.size
77 | end
78 |
79 | def test_is_member_simple_group
80 | group = GitHub::Ldap::PosixGroup.new(@ldap, @simple_group)
81 | user = @ldap.domain("uid=user1,ou=People,dc=github,dc=com").bind
82 |
83 | assert group.is_member?(user),
84 | "Expected user in the memberUid list to be a member of the posixgroup"
85 | end
86 |
87 | def test_is_member_combined_group
88 | group = GitHub::Ldap::PosixGroup.new(@ldap, @one_level_deep_group)
89 | user = @ldap.domain("uid=user1,ou=People,dc=github,dc=com").bind
90 |
91 | assert group.is_member?(user),
92 | "Expected user in a subgroup to be a member of the posixgroup"
93 | end
94 |
95 | def test_is_not_member_simple_group
96 | group = GitHub::Ldap::PosixGroup.new(@ldap, @simple_group)
97 | user = @ldap.domain("uid=user10,ou=People,dc=github,dc=com").bind
98 |
99 | refute group.is_member?(user),
100 | "Expected user to not be member when her uid is not in the list of memberUid"
101 | end
102 |
103 | def test_is_member_combined_group
104 | group = GitHub::Ldap::PosixGroup.new(@ldap, @one_level_deep_group)
105 | user = @ldap.domain("uid=user10,ou=People,dc=github,dc=com").bind
106 |
107 | refute group.is_member?(user),
108 | "Expected user to not be member when she's not member of any subgroup"
109 | end
110 |
111 | def test_empty_posix_group
112 | group = GitHub::Ldap::PosixGroup.new(@ldap, @empty_group)
113 |
114 | assert group.members.empty?,
115 | "Expected members to be an empty array"
116 | end
117 | end
118 |
--------------------------------------------------------------------------------
/test/referral_chaser_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | class GitHubLdapReferralChaserTestCases < GitHub::Ldap::Test
4 |
5 | def setup
6 | @ldap = GitHub::Ldap.new(options)
7 | @chaser = GitHub::Ldap::ReferralChaser.new(@ldap)
8 | end
9 |
10 | def test_creates_referral_with_connection_credentials
11 | @ldap.expects(:search).yields({ search_referrals: ["ldap://dc1.ghe.local/"]}).returns([])
12 |
13 | referral = mock("GitHub::Ldap::ReferralChaser::Referral")
14 | referral.stubs(:search).returns([])
15 |
16 | GitHub::Ldap::ReferralChaser::Referral.expects(:new)
17 | .with("ldap://dc1.ghe.local/", "uid=admin,dc=github,dc=com", "passworD1", options[:port])
18 | .returns(referral)
19 |
20 | @chaser.search({})
21 | end
22 |
23 | def test_creates_referral_with_default_port
24 | @ldap.expects(:search).yields({
25 | search_referrals: ["ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"]
26 | }).returns([])
27 |
28 | stub_referral_connection = mock("GitHub::Ldap")
29 | stub_referral_connection.stubs(:search).returns([])
30 | GitHub::Ldap::ConnectionCache.expects(:get_connection).with(has_entry(port: options[:port])).returns(stub_referral_connection)
31 | chaser = GitHub::Ldap::ReferralChaser.new(@ldap)
32 | chaser.search({})
33 | end
34 |
35 | def test_creates_referral_for_first_referral_string
36 | @ldap.expects(:search).multiple_yields([
37 | { search_referrals:
38 | ["ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local",
39 | "ldap://dc2.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"]
40 | }
41 | ],[
42 | { search_referrals:
43 | ["ldap://dc3.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local",
44 | "ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"]
45 | }
46 | ]).returns([])
47 |
48 | referral = mock("GitHub::Ldap::ReferralChaser::Referral")
49 | referral.stubs(:search).returns([])
50 |
51 | GitHub::Ldap::ReferralChaser::Referral.expects(:new)
52 | .with(
53 | "ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local",
54 | "uid=admin,dc=github,dc=com",
55 | "passworD1",
56 | options[:port])
57 | .returns(referral)
58 |
59 | @chaser.search({})
60 | end
61 |
62 | def test_returns_referral_search_results
63 | @ldap.expects(:search).multiple_yields([
64 | { search_referrals:
65 | ["ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local",
66 | "ldap://dc2.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"]
67 | }
68 | ],[
69 | { search_referrals:
70 | ["ldap://dc3.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local",
71 | "ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"]
72 | }
73 | ]).returns([])
74 |
75 | referral = mock("GitHub::Ldap::ReferralChaser::Referral")
76 | referral.expects(:search).returns(["result", "result"])
77 |
78 | GitHub::Ldap::ReferralChaser::Referral.expects(:new).returns(referral)
79 |
80 | results = @chaser.search({})
81 | assert_equal(["result", "result"], results)
82 | end
83 |
84 | def test_handle_blank_url_string_in_referral
85 | @ldap.expects(:search).yields({ search_referrals: [""] })
86 |
87 | results = @chaser.search({})
88 | assert_equal([], results)
89 | end
90 |
91 | def test_returns_referral_search_results
92 | @ldap.expects(:search).yields({ foo: ["not a referral"] })
93 |
94 | GitHub::Ldap::ReferralChaser::Referral.expects(:new).never
95 | results = @chaser.search({})
96 | end
97 |
98 | def test_referral_should_use_host_from_referral_string
99 | GitHub::Ldap::ConnectionCache.expects(:get_connection).with(has_entry(host: "dc4.ghe.local"))
100 | GitHub::Ldap::ReferralChaser::Referral.new("ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", "", "")
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/test/support/vm/activedirectory/.gitignore:
--------------------------------------------------------------------------------
1 | env.sh
2 |
--------------------------------------------------------------------------------
/test/support/vm/activedirectory/README.md:
--------------------------------------------------------------------------------
1 | # Local ActiveDirectory Integration Testing
2 |
3 | Integration tests are not run for ActiveDirectory in continuous integration
4 | because we cannot install a Windows VM on TravisCI. To test ActiveDirectory,
5 | configure a local VM with AD running (this is left as an exercise for the
6 | reader).
7 |
8 | To run integration tests against the local ActiveDirectory VM, from the project
9 | root run:
10 |
11 | ``` bash
12 | # duplicate example env.sh for specific config
13 | $ cp test/support/vm/activedirectory/env.sh{.example,}
14 |
15 | # edit env.sh and fill in with your VM's values, then
16 | $ source test/support/vm/activedirectory/env.sh
17 |
18 | # run all tests against AD
19 | $ time bundle exec rake
20 |
21 | # run a specific test file against AD
22 | $ time bundle exec ruby test/membership_validators/active_directory_test.rb
23 |
24 | # reset environment to test other LDAP servers
25 | $ source test/support/vm/activedirectory/reset-env.sh
26 | ```
27 |
--------------------------------------------------------------------------------
/test/support/vm/activedirectory/env.sh.example:
--------------------------------------------------------------------------------
1 | # Copy this to ad-env.sh, and fill in with your own values
2 |
3 | export TESTENV=activedirectory
4 | export INTEGRATION_HOST=123.123.123.123
5 | export INTEGRATION_PORT=389
6 | export INTEGRATION_USER="CN=Administrator,CN=Users,DC=ad,DC=example,DC=com"
7 | export INTEGRATION_PASSWORD='passworD1'
8 | export INTEGRATION_SEARCH_DOMAINS='CN=Users,DC=example,DC=com'
9 |
--------------------------------------------------------------------------------
/test/support/vm/activedirectory/reset-env.sh:
--------------------------------------------------------------------------------
1 | unset TESTENV
2 | unset INTEGRATION_HOST
3 | unset INTEGRATION_PORT
4 | unset INTEGRATION_USER
5 | unset INTEGRATION_PASSWORD
6 | unset INTEGRATION_SEARCH_DOMAINS
7 |
--------------------------------------------------------------------------------
/test/support/vm/openldap/.gitignore:
--------------------------------------------------------------------------------
1 | /.vagrant
2 |
--------------------------------------------------------------------------------
/test/support/vm/openldap/README.md:
--------------------------------------------------------------------------------
1 | # Local OpenLDAP Integration Testing
2 |
3 | Set up a [Vagrant](http://www.vagrantup.com/) VM to run tests against OpenLDAP locally.
4 |
5 | To run tests against OpenLDAP (instead of ApacheDS) locally:
6 |
7 | ``` bash
8 | # start VM (from the correct directory)
9 | $ cd test/support/vm/openldap/
10 | $ vagrant up
11 |
12 | # get the IP address of the VM
13 | $ ip=$(vagrant ssh -- "ifconfig eth1 | grep -o -E '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1")
14 |
15 | # change back to root project directory
16 | $ cd ../../../..
17 |
18 | # run all tests against OpenLDAP
19 | $ time TESTENV=openldap INTEGRATION_HOST=$ip bundle exec rake
20 |
21 | # run a specific test file against OpenLDAP
22 | $ time TESTENV=openldap INTEGRATION_HOST=$ip bundle exec ruby test/membership_validators/recursive_test.rb
23 |
24 | # run OpenLDAP tests by default
25 | $ export TESTENV=openldap
26 | $ export TESTENV=$ip
27 |
28 | # now run tests without having to set ENV variables
29 | $ time bundle exec rake
30 | ```
31 |
32 | You may need to `gem install vagrant` first in order to provision the VM.
33 |
--------------------------------------------------------------------------------
/test/support/vm/openldap/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
5 | VAGRANTFILE_API_VERSION = "2"
6 |
7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
8 | config.vm.hostname = "openldap.github.org"
9 |
10 | config.vm.box = "hashicorp/precise64"
11 |
12 | config.vm.network "private_network", type: :dhcp
13 |
14 | config.ssh.forward_agent = true
15 |
16 | # config.vm.provision "shell", inline: "apt-get update; exec env /vagrant_data/script/install-openldap"
17 | config.vm.provision "shell", inline: 'echo "HIIIIIII"', run: "always"
18 |
19 | config.vm.synced_folder "../../../..", "/vagrant_data"
20 |
21 | config.vm.provider "vmware_fusion" do |vb, override|
22 | override.vm.box = "hashicorp/precise64"
23 | vb.memory = 4596
24 | vb.vmx["displayname"] = "integration tests vm"
25 | vb.vmx["numvcpus"] = "2"
26 | end
27 |
28 | config.vm.provider "virtualbox" do |vb, override|
29 | vb.memory = 4096
30 | vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
31 | vb.customize ["modifyvm", :id, "--chipset", "ich9"]
32 | vb.customize ["modifyvm", :id, "--vram", "16"]
33 | end
34 |
35 | end
36 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | __dir__ = File.expand_path(File.dirname(__FILE__))
2 | __lib__ = File.expand_path('lib', File.dirname(__FILE__))
3 |
4 | $LOAD_PATH << __dir__ unless $LOAD_PATH.include?(__dir__)
5 | $LOAD_PATH << __lib__ unless $LOAD_PATH.include?(__lib__)
6 |
7 | require 'pathname'
8 | FIXTURES = Pathname(File.expand_path('fixtures', __dir__))
9 |
10 | require 'github/ldap'
11 | require 'github/ldap/server'
12 |
13 | require 'minitest/mock'
14 | require 'minitest/autorun'
15 |
16 | require 'mocha/minitest'
17 |
18 | if ENV.fetch('TESTENV', "apacheds") == "apacheds"
19 | # Make sure we clean up running test server
20 | # NOTE: We need to do this manually since its internal `at_exit` hook
21 | # collides with Minitest's autorun at_exit handling, hence this hook.
22 | Minitest.after_run do
23 | GitHub::Ldap.stop_server
24 | end
25 | end
26 |
27 | class GitHub::Ldap::Test < Minitest::Test
28 | def self.test_env
29 | ENV.fetch("TESTENV", "apacheds")
30 | end
31 |
32 | def self.run(reporter, options = {})
33 | start_server
34 | result = super
35 | stop_server
36 | result
37 | end
38 |
39 | def self.stop_server
40 | if test_env == "apacheds"
41 | # see Minitest.after_run hook above.
42 | # GitHub::Ldap.stop_server
43 | end
44 | end
45 |
46 | def self.test_server_options
47 | {
48 | custom_schemas: FIXTURES.join('posixGroup.schema.ldif').to_s,
49 | user_fixtures: FIXTURES.join('common/seed.ldif').to_s,
50 | allow_anonymous: true,
51 | verbose: ENV.fetch("VERBOSE", "0") == "1"
52 | }
53 | end
54 |
55 | def self.start_server
56 | if test_env == "apacheds"
57 | # skip this if a server has already been started
58 | return if GitHub::Ldap.ldap_server
59 |
60 | GitHub::Ldap.start_server(test_server_options)
61 | end
62 | end
63 |
64 | def options
65 | @service = MockInstrumentationService.new
66 | @options ||=
67 | case self.class.test_env
68 | when "apacheds"
69 | GitHub::Ldap.server_options.merge \
70 | admin_user: 'uid=admin,dc=github,dc=com',
71 | admin_password: 'passworD1',
72 | host: 'localhost',
73 | uid: 'uid',
74 | instrumentation_service: @service
75 | when "openldap"
76 | {
77 | host: ENV.fetch("INTEGRATION_HOST", "localhost"),
78 | port: 389,
79 | admin_user: 'uid=admin,dc=github,dc=com',
80 | admin_password: 'passworD1',
81 | search_domains: %w(dc=github,dc=com),
82 | uid: 'uid',
83 | instrumentation_service: @service
84 | }
85 | when "activedirectory"
86 | {
87 | host: ENV.fetch("INTEGRATION_HOST"),
88 | port: ENV.fetch("INTEGRATION_PORT", 389),
89 | admin_user: ENV.fetch("INTEGRATION_USER"),
90 | admin_password: ENV.fetch("INTEGRATION_PASSWORD"),
91 | search_domains: ENV.fetch("INTEGRATION_SEARCH_DOMAINS"),
92 | instrumentation_service: @service
93 | }
94 | end
95 | end
96 | end
97 |
98 | class GitHub::Ldap::UnauthenticatedTest < GitHub::Ldap::Test
99 | def options
100 | @options ||= begin
101 | super.delete_if {|k, _| [:admin_user, :admin_password].include?(k)}
102 | end
103 | end
104 | end
105 |
106 | class MockInstrumentationService
107 | def initialize
108 | @events = {}
109 | end
110 |
111 | def instrument(event, payload)
112 | result = yield(payload)
113 | @events[event] ||= []
114 | @events[event] << [payload, result]
115 | result
116 | end
117 |
118 | def subscribe(event)
119 | @events[event] ||= []
120 | @events[event]
121 | end
122 | end
123 |
--------------------------------------------------------------------------------
/test/url_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | class GitHubLdapURLTestCases < GitHub::Ldap::Test
4 |
5 | def setup
6 | @url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local:123/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local?cn,mail,telephoneNumber?base?(cn=Charlie)")
7 | end
8 |
9 | def test_host
10 | assert_equal "dc4.ghe.local", @url.host
11 | end
12 |
13 | def test_port
14 | assert_equal 123, @url.port
15 | end
16 |
17 | def test_scheme
18 | assert_equal "ldap", @url.scheme
19 | end
20 |
21 | def test_default_port
22 | url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local?attributes?scope?filter")
23 | assert_equal 389, url.port
24 | end
25 |
26 | def test_simple_url
27 | url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local")
28 | assert_equal 389, url.port
29 | assert_equal "dc4.ghe.local", url.host
30 | assert_equal "ldap", url.scheme
31 | assert_equal "", url.dn
32 | assert_equal nil, url.attributes
33 | assert_equal nil, url.filter
34 | assert_equal nil, url.scope
35 | end
36 |
37 | def test_invalid_scheme
38 | ex = assert_raises(GitHub::Ldap::URL::InvalidLdapURLException) do
39 | GitHub::Ldap::URL.new("http://dc4.ghe.local")
40 | end
41 | assert_equal("Invalid LDAP URL: http://dc4.ghe.local", ex.message)
42 | end
43 |
44 | def test_invalid_url
45 | ex = assert_raises(GitHub::Ldap::URL::InvalidLdapURLException) do
46 | GitHub::Ldap::URL.new("not a url")
47 | end
48 | assert_equal("Invalid LDAP URL: not a url", ex.message)
49 | end
50 |
51 | def test_parse_dn
52 | assert_equal "CN=Maggie Mae,CN=Users,DC=dc4,DC=ghe,DC=local", @url.dn
53 | end
54 |
55 | def test_parse_attributes
56 | assert_equal "cn,mail,telephoneNumber", @url.attributes
57 | end
58 |
59 | def test_parse_filter
60 | assert_equal "(cn=Charlie)", @url.filter
61 | end
62 |
63 | def test_parse_scope
64 | assert_equal "base", @url.scope
65 | end
66 |
67 | def test_default_scope
68 | url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/base_dn?cn=joe??filter")
69 | assert_equal "", url.scope
70 | end
71 |
72 | def test_net_ldap_scopes
73 | sub_scope_url = GitHub::Ldap::URL.new("ldap://ghe.local/base_dn?cn=joe?sub?filter")
74 | one_scope_url = GitHub::Ldap::URL.new("ldap://ghe.local/base_dn?cn=joe?one?filter")
75 | base_scope_url = GitHub::Ldap::URL.new("ldap://ghe.local/base_dn?cn=joe?base?filter")
76 | default_scope_url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/base_dn?cn=joe??filter")
77 | invalid_scope_url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/base_dn?cn=joe?invalid?filter")
78 |
79 | assert_equal Net::LDAP::SearchScope_BaseObject, base_scope_url.net_ldap_scope
80 | assert_equal Net::LDAP::SearchScope_SingleLevel, one_scope_url.net_ldap_scope
81 | assert_equal Net::LDAP::SearchScope_WholeSubtree, sub_scope_url.net_ldap_scope
82 | assert_equal Net::LDAP::SearchScope_BaseObject, default_scope_url.net_ldap_scope
83 | assert_equal Net::LDAP::SearchScope_BaseObject, invalid_scope_url.net_ldap_scope
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/test/user_search/active_directory_test.rb:
--------------------------------------------------------------------------------
1 | require_relative '../test_helper'
2 |
3 | class GitHubLdapActiveDirectoryUserSearchTests < GitHub::Ldap::Test
4 |
5 | def test_global_catalog_returns_empty_array_for_no_results
6 | ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev'))
7 | ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(ldap)
8 |
9 | mock_global_catalog_connection = mock("GitHub::Ldap::UserSearch::GlobalCatalog")
10 | mock_global_catalog_connection.expects(:search).returns(nil)
11 | ad_user_search.expects(:global_catalog_connection).returns(mock_global_catalog_connection)
12 | results = ad_user_search.perform("login", "CN=Joe", "uid", {})
13 | assert_equal [], results
14 | end
15 |
16 | def test_global_catalog_returns_array_of_results
17 | ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev'))
18 | ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(ldap)
19 |
20 | mock_global_catalog_connection = mock("GitHub::Ldap::UserSearch::GlobalCatalog")
21 | stub_entry = mock("Net::LDAP::Entry")
22 |
23 | mock_global_catalog_connection.expects(:search).returns([stub_entry])
24 | ad_user_search.expects(:global_catalog_connection).returns(mock_global_catalog_connection)
25 |
26 | results = ad_user_search.perform("login", "CN=Joe", "uid", {})
27 | assert_equal [stub_entry], results
28 | end
29 |
30 | def test_searches_with_empty_base_dn
31 | ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev'))
32 | ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(ldap)
33 |
34 | mock_global_catalog_connection = mock("GitHub::Ldap::UserSearch::GlobalCatalog")
35 | mock_global_catalog_connection.expects(:search).with(has_entry(:base => ""))
36 | ad_user_search.expects(:global_catalog_connection).returns(mock_global_catalog_connection)
37 | ad_user_search.perform("login", "CN=Joe", "uid", {})
38 | end
39 |
40 | def test_global_catalog_default_settings
41 | ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev'))
42 | global_catalog = GitHub::Ldap::UserSearch::GlobalCatalog.connection(ldap)
43 | instrumentation_service = global_catalog.instance_variable_get(:@instrumentation_service)
44 |
45 | auth = global_catalog.instance_variable_get(:@auth)
46 | assert_equal :simple, auth[:method]
47 | assert_equal "uid=admin,dc=github,dc=com", auth[:username]
48 | assert_equal "passworD1", auth[:password]
49 | assert_equal "ghe.dev", global_catalog.host
50 | assert_equal 3268, global_catalog.port
51 | assert_equal "MockInstrumentationService", instrumentation_service.class.name
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/test/user_search/default_test.rb:
--------------------------------------------------------------------------------
1 | require_relative '../test_helper'
2 |
3 | class GitHubLdapActiveDirectoryUserSearchTests < GitHub::Ldap::Test
4 | def setup
5 | @ldap = GitHub::Ldap.new(options)
6 | @default_user_search = GitHub::Ldap::UserSearch::Default.new(@ldap)
7 | end
8 |
9 | def test_default_search_options
10 | @ldap.expects(:search).with(has_entries(
11 | attributes: [],
12 | size: 1,
13 | paged_searches_supported: true,
14 | base: "CN=HI,CN=McDunnough",
15 | filter: kind_of(Net::LDAP::Filter)
16 | ))
17 | @default_user_search.perform("","CN=HI,CN=McDunnough","",{})
18 | end
19 | end
20 |
--------------------------------------------------------------------------------