├── .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 | ![Build Status](https://travis-ci.org/github/github-ldap.png?branch=master) 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 | --------------------------------------------------------------------------------