diff --git a/.travis.yml b/.travis.yml index 8f17123..d9fd02b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,30 @@ language: ruby rvm: - - 1.9.3 - - 2.1.0 + - 2.0.0 + - 2.1.0 +env: + - TESTENV=openldap + - TESTENV=apacheds + +# https://docs.travis-ci.com/user/hosts/ +addons: + hosts: + - ad1.ghe.dev + - ad2.ghe.dev + +before_install: + - echo "deb http://ftp.br.debian.org/debian stable main" | sudo tee -a /etc/apt/sources.list + - sudo apt-get update + +install: + - if [ "$TESTENV" = "openldap" ]; then ./script/install-openldap; fi + - bundle install + +script: + - ./script/cibuild-$TESTENV + +matrix: + fast_finish: true notifications: email: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e082762 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,58 @@ +# CHANGELOG + +# v1.10.1 + +* Bump net-ldap to 0.16.0 + +# v1.10.0 + +* Bump net-ldap to 0.15.0 [#92](https://github.com/github/github-ldap/pull/92) + +# v1.9.0 + +* Update net-ldap dependency to `~> 0.11.0` [#84](https://github.com/github/github-ldap/pull/84) + +# v1.8.2 + +* Ignore case when comparing ActiveDirectory DNs [#82](https://github.com/github/github-ldap/pull/82) + +# v1.8.1 + +* Expand supported ActiveDirectory capabilities to include Windows Server 2003 [#80](https://github.com/github/github-ldap/pull/80) + +# v1.8.0 + +* Optimize Recursive *Member Search* strategy [#78](https://github.com/github/github-ldap/pull/78) + +# v1.7.1 + +* Add Active Directory group filter [#75](https://github.com/github/github-ldap/pull/75) + +## v1.7.0 + +* Accept `:depth` option for Recursive membership validator strategy instance [#73](https://github.com/github/github-ldap/pull/73) +* Deprecate `depth` argument to `Recursive` membership validator `perform` method +* Bump net-ldap dependency to 0.10.0 at minimum [#72](https://github.com/github/github-ldap/pull/72) + +## v1.6.0 + +* Expose `GitHub::Ldap::Group.group?` for testing if entry is a group [#67](https://github.com/github/github-ldap/pull/67) +* 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) +* Simplify *Member Search* and *Membership Validation* search strategy configuration, detection, and default behavior [#70](https://github.com/github/github-ldap/pull/70) + +## v1.5.0 + +* 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) +* Document local integration testing with Active Directory [#61](https://github.com/github/github-ldap/pull/61) + +## v1.4.0 + +* Document constructor options [#57](https://github.com/github/github-ldap/pull/57) +* [CI] Add Vagrant box for running tests against OpenLDAP locally [#55](https://github.com/github/github-ldap/pull/55) +* Run all tests, including those in subdirectories [#54](https://github.com/github/github-ldap/pull/54) +* Add ActiveDirectory membership validator [#52](https://github.com/github/github-ldap/pull/52) +* Merge dev-v2 branch into master [#50](https://github.com/github/github-ldap/pull/50) +* Pass through search options for GitHub::Ldap::Domain#user? [#51](https://github.com/github/github-ldap/pull/51) +* Fix membership validation tests [#49](https://github.com/github/github-ldap/pull/49) +* Add CI build for OpenLDAP integration [#48](https://github.com/github/github-ldap/pull/48) +* Membership Validators [#45](https://github.com/github/github-ldap/pull/45) diff --git a/Gemfile b/Gemfile index ab76291..a409814 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,11 @@ source 'https://rubygems.org' # Specify your gem's dependencies in github-ldap.gemspec gemspec + +group :test, :development do + gem "byebug", :platforms => [:mri_20, :mri_21] +end + +group :test do + gem "mocha" +end diff --git a/README.md b/README.md index 66f7073..eb5fb01 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Build Status](https://travis-ci.org/github/github-ldap.png) +![Build Status](https://travis-ci.org/github/github-ldap.png?branch=master) # Github::Ldap @@ -28,6 +28,7 @@ There are a few configuration options required to use this adapter: * host: is the host address where the ldap server lives. * port: is the port where the ldap server lives. +* 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. * encryption: is the encryption protocol, disabled by default. The valid options are `ssl` and `tls`. * uid: is the field name in the ldap server used to authenticate your users, in ActiveDirectory this is `sAMAccountName`. @@ -42,6 +43,8 @@ Initialize a new adapter using those required options: ldap = GitHub::Ldap.new options ``` +See GitHub::Ldap#initialize for additional options. + ### Querying Searches are performed against an individual domain base, so the first step is to get a new `GitHub::Ldap::Domain` object for the connection: @@ -128,3 +131,15 @@ end 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request + +## Releasing + +This section is for gem maintainers to cut a new version of the gem. See +[jch/release-scripts](https://github.com/jch/release-scripts) for original +source of release scripts. + +* Create a new branch from `master` named `release-x.y.z`, where `x.y.z` is the version to be released +* Update `github-ldap.gemspec` to x.y.z following [semver](http://semver.org) +* Run `script/changelog` and paste the draft into `CHANGELOG.md`. Edit as needed +* Create pull request to solict feedback +* After merging the pull request, on the master branch, run `script/release` diff --git a/Rakefile b/Rakefile index 940d70f..5b19c5a 100644 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,7 @@ require 'rake/testtask' Rake::TestTask.new do |t| t.libs << "test" - t.pattern = "test/*_test.rb" + t.pattern = "test/**/*_test.rb" end task :default => :test diff --git a/github-ldap.gemspec b/github-ldap.gemspec index 65c7c70..a2dad47 100644 --- a/github-ldap.gemspec +++ b/github-ldap.gemspec @@ -2,11 +2,11 @@ Gem::Specification.new do |spec| spec.name = "github-ldap" - spec.version = "1.3.6" - spec.authors = ["David Calavera"] - spec.email = ["david.calavera@gmail.com"] - spec.description = %q{Ldap authentication for humans} - spec.summary = %q{Ldap client authentication wrapper without all the boilerplate} + spec.version = "1.10.1" + spec.authors = ["David Calavera", "Matt Todd"] + spec.email = ["david.calavera@gmail.com", "chiology@gmail.com"] + spec.description = %q{LDAP authentication for humans} + spec.summary = %q{LDAP client authentication wrapper without all the boilerplate} spec.homepage = "https://github.com/github/github-ldap" spec.license = "MIT" @@ -15,7 +15,7 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] - spec.add_dependency 'net-ldap', '~> 0.8.0' + spec.add_dependency 'net-ldap', '> 0.16.0' spec.add_development_dependency "bundler", "~> 1.3" spec.add_development_dependency 'ladle' diff --git a/lib/github/ldap.rb b/lib/github/ldap.rb index 9844ceb..33e6627 100644 --- a/lib/github/ldap.rb +++ b/lib/github/ldap.rb @@ -1,19 +1,31 @@ +require 'net/ldap' +require 'forwardable' + +require 'github/ldap/filter' +require 'github/ldap/domain' +require 'github/ldap/group' +require 'github/ldap/posix_group' +require 'github/ldap/virtual_group' +require 'github/ldap/virtual_attributes' +require 'github/ldap/instrumentation' +require 'github/ldap/member_search' +require 'github/ldap/membership_validators' +require 'github/ldap/user_search/default' +require 'github/ldap/user_search/active_directory' +require 'github/ldap/connection_cache' +require 'github/ldap/referral_chaser' +require 'github/ldap/url' + module GitHub class Ldap - require 'net/ldap' - require 'forwardable' - require 'github/ldap/filter' - require 'github/ldap/domain' - require 'github/ldap/group' - require 'github/ldap/posix_group' - require 'github/ldap/virtual_group' - require 'github/ldap/virtual_attributes' - require 'github/ldap/instrumentation' - include Instrumentation extend Forwardable + # Internal: The capability required to use ActiveDirectory features. + # See: http://msdn.microsoft.com/en-us/library/cc223359.aspx. + ACTIVE_DIRECTORY_V51_OID = "1.2.840.113556.1.4.1670".freeze + # Utility method to get the last operation result with a human friendly message. # # Returns an OpenStruct with `code` and `message`. @@ -31,16 +43,58 @@ class Ldap # # Returns the return value of the block. def_delegator :@connection, :open + def_delegator :@connection, :host attr_reader :uid, :search_domains, :virtual_attributes, - :instrumentation_service + :membership_validator, + :member_search_strategy, + :instrumentation_service, + :user_search_strategy, + :connection, + :admin_user, + :admin_password, + :port + # Build a new GitHub::Ldap instance + # + # ## Connection + # + # host: required string ldap server host address + # port: required string or number ldap server port + # hosts: an enumerable of pairs of hosts and corresponding ports with + # which to attempt opening connections (default [[host, port]]). Overrides + # host and port if set. + # encryption: optional string. `ssl` or `tls`. nil by default + # tls_options: optional hash with TLS options for encrypted connections. + # Empty by default. See http://ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html + # for available values + # admin_user: optional string ldap administrator user dn for authentication + # admin_password: optional string ldap administrator user password + # + # ## Behavior + # + # uid: optional field name used to authenticate users. Defaults to `sAMAccountName` (what ActiveDirectory uses) + # virtual_attributes: optional. boolean true to use server's virtual attributes. Hash to specify custom mapping. Default false. + # recursive_group_search_fallback: optional boolean whether membership checks should recurse into nested groups when virtual attributes aren't enabled. Default false. + # posix_support: optional boolean `posixGroup` support. Default true. + # search_domains: optional array of string bases to search through + # + # ## Diagnostics + # + # instrumentation_service: optional ActiveSupport::Notifications compatible object + # def initialize(options = {}) @uid = options[:uid] || "sAMAccountName" + # Keep a reference to these as default auth for a Global Catalog if needed + @admin_user = options[:admin_user] + @admin_password = options[:admin_password] + @port = options[:port] + @connection = Net::LDAP.new({ host: options[:host], port: options[:port], + hosts: options[:hosts], instrumentation_service: options[:instrumentation_service] }) @@ -48,7 +102,7 @@ def initialize(options = {}) @connection.authenticate(options[:admin_user], options[:admin_password]) end - if encryption = check_encryption(options[:encryption]) + if encryption = check_encryption(options[:encryption], options[:tls_options]) @connection.encryption(encryption) end @@ -64,6 +118,12 @@ def initialize(options = {}) # when a base is not explicitly provided. @search_domains = Array(options[:search_domains]) + # configure both the membership validator and the member search strategies + configure_search_strategy(options[:search_strategy]) + + # configure the strategy used by Domain#user? to look up a user entry for login + configure_user_search_strategy(options[:user_search_strategy]) + # enables instrumenting queries @instrumentation_service = options[:instrumentation_service] end @@ -159,19 +219,38 @@ def search(options, &block) end end + # Internal: Searches the host LDAP server's Root DSE for capabilities and + # extensions. + # + # Returns a Net::LDAP::Entry object. + def capabilities + @capabilities ||= + instrument "capabilities.github_ldap" do |payload| + begin + @connection.search_root_dse + rescue Net::LDAP::Error => error + payload[:error] = error + # stubbed result + Net::LDAP::Entry.new + end + end + end + # Internal - Determine whether to use encryption or not. # # encryption: is the encryption method, either 'ssl', 'tls', 'simple_tls' or 'start_tls'. + # tls_options: is the options hash for tls encryption method # # Returns the real encryption type. - def check_encryption(encryption) + def check_encryption(encryption, tls_options = {}) return unless encryption + tls_options ||= {} case encryption.downcase.to_sym when :ssl, :simple_tls - :simple_tls + { method: :simple_tls, tls_options: tls_options } when :tls, :start_tls - :start_tls + { method: :start_tls, tls_options: tls_options } end end @@ -191,5 +270,101 @@ def configure_virtual_attributes(attributes) VirtualAttributes.new(false) end end + + # Internal: Configure the member search and membership validation strategies. + # + # TODO: Inline the logic in these two methods here. + # + # Returns nothing. + def configure_search_strategy(strategy = nil) + # configure which strategy should be used to validate user membership + configure_membership_validation_strategy(strategy) + + # configure which strategy should be used for member search + configure_member_search_strategy(strategy) + end + + # Internal: Configure the membership validation strategy. + # + # If no known strategy is provided, detects ActiveDirectory capabilities or + # falls back to the Recursive strategy by default. + # + # Returns the membership validator strategy Class. + def configure_membership_validation_strategy(strategy = nil) + @membership_validator = + case strategy.to_s + when "classic" + GitHub::Ldap::MembershipValidators::Classic + when "recursive" + GitHub::Ldap::MembershipValidators::Recursive + when "active_directory" + GitHub::Ldap::MembershipValidators::ActiveDirectory + else + # fallback to detection, defaulting to recursive strategy + if active_directory_capability? + GitHub::Ldap::MembershipValidators::ActiveDirectory + else + GitHub::Ldap::MembershipValidators::Recursive + end + end + end + + # Internal: Set the user search strategy that will be used by + # Domain#user?. + # + # strategy - Can be either 'default' or 'global_catalog'. + # 'default' strategy will search the configured + # domain controller with a search base relative + # to the controller's domain context. + # 'global_catalog' will search the entire forest + # using Active Directory's Global Catalog + # functionality. + def configure_user_search_strategy(strategy) + @user_search_strategy = + case strategy.to_s + when "default" + GitHub::Ldap::UserSearch::Default.new(self) + when "global_catalog" + GitHub::Ldap::UserSearch::ActiveDirectory.new(self) + else + GitHub::Ldap::UserSearch::Default.new(self) + end + end + + # Internal: Configure the member search strategy. + # + # + # If no known strategy is provided, detects ActiveDirectory capabilities or + # falls back to the Recursive strategy by default. + # + # Returns the selected strategy Class. + def configure_member_search_strategy(strategy = nil) + @member_search_strategy = + case strategy.to_s + when "classic" + GitHub::Ldap::MemberSearch::Classic + when "recursive" + GitHub::Ldap::MemberSearch::Recursive + when "active_directory" + GitHub::Ldap::MemberSearch::ActiveDirectory + else + # fallback to detection, defaulting to recursive strategy + if active_directory_capability? + GitHub::Ldap::MemberSearch::ActiveDirectory + else + GitHub::Ldap::MemberSearch::Recursive + end + end + end + + # Internal: Detect whether the LDAP host is an ActiveDirectory server. + # + # See: http://msdn.microsoft.com/en-us/library/cc223359.aspx. + # + # Returns true if the host is an ActiveDirectory server, false otherwise. + def active_directory_capability? + capabilities[:supportedcapabilities].include?(ACTIVE_DIRECTORY_V51_OID) + end + private :active_directory_capability? end end diff --git a/lib/github/ldap/connection_cache.rb b/lib/github/ldap/connection_cache.rb new file mode 100644 index 0000000..d2feab9 --- /dev/null +++ b/lib/github/ldap/connection_cache.rb @@ -0,0 +1,26 @@ +module GitHub + class Ldap + + # A simple cache of GitHub::Ldap objects to prevent creating multiple + # instances of connections that point to the same URI/host. + class ConnectionCache + + # Public - Create or return cached instance of GitHub::Ldap created with options, + # where the cache key is the value of options[:host]. + # + # options - Initialization attributes suitable for creating a new connection with + # GitHub::Ldap.new(options) + # + # Returns true or false. + def self.get_connection(options={}) + @cache ||= self.new + @cache.get_connection(options) + end + + def get_connection(options) + @connections ||= {} + @connections[options[:host]] ||= GitHub::Ldap.new(options) + end + end + end +end diff --git a/lib/github/ldap/domain.rb b/lib/github/ldap/domain.rb index 8085c44..07af950 100644 --- a/lib/github/ldap/domain.rb +++ b/lib/github/ldap/domain.rb @@ -110,11 +110,12 @@ def valid_login?(login, password) # Check if a user exists based in the `uid`. # # login: is the user's login + # search_options: Net::LDAP#search compatible options to pass through # # Returns the user if the login matches any `uid`. # Returns nil if there are no matches. - def user?(login) - search(filter: login_filter(@uid, login), size: 1).first + def user?(login, search_options = {}) + @ldap.user_search_strategy.perform(login, @base_name, @uid, search_options).first end # Check if a user can be bound with a password. @@ -159,8 +160,11 @@ def search(options, &block) # Get the entry for this domain. # # Returns a Net::LDAP::Entry - def bind - search(size: 1, scope: Net::LDAP::SearchScope_BaseObject).first + def bind(options = {}) + options[:size] = 1 + options[:scope] = Net::LDAP::SearchScope_BaseObject + options[:attributes] ||= [] + search(options).first end end end diff --git a/lib/github/ldap/filter.rb b/lib/github/ldap/filter.rb index a238642..6f62af3 100644 --- a/lib/github/ldap/filter.rb +++ b/lib/github/ldap/filter.rb @@ -3,7 +3,8 @@ class Ldap module Filter ALL_GROUPS_FILTER = Net::LDAP::Filter.eq("objectClass", "groupOfNames") | Net::LDAP::Filter.eq("objectClass", "groupOfUniqueNames") | - Net::LDAP::Filter.eq("objectClass", "posixGroup") + Net::LDAP::Filter.eq("objectClass", "posixGroup") | + Net::LDAP::Filter.eq("objectClass", "group") MEMBERSHIP_NAMES = %w(member uniqueMember) @@ -20,16 +21,18 @@ def group_filter(group_names) # Filter to check group membership. # - # entry: finds groups this Net::LDAP::Entry is a member of (optional) + # entry: finds groups this entry is a member of (optional) + # Expects a Net::LDAP::Entry or String DN. # # Returns a Net::LDAP::Filter. def member_filter(entry = nil) if entry + entry = entry.dn if entry.respond_to?(:dn) MEMBERSHIP_NAMES. - map {|n| Net::LDAP::Filter.eq(n, entry.dn) }.reduce(:|) + map {|n| Net::LDAP::Filter.eq(n, entry) }.reduce(:|) else MEMBERSHIP_NAMES. - map {|n| Net::LDAP::Filter.pres(n) }. reduce(:|) + map {|n| Net::LDAP::Filter.pres(n) }. reduce(:|) end end @@ -41,10 +44,16 @@ def member_filter(entry = nil) # uid_attr: specifies the memberUid attribute to match with # # Returns a Net::LDAP::Filter or nil if no entry has no UID set. - def posix_member_filter(entry, uid_attr) - if !entry[uid_attr].empty? - entry[uid_attr].map { |uid| Net::LDAP::Filter.eq("memberUid", uid) }. - reduce(:|) + def posix_member_filter(entry_or_uid, uid_attr = nil) + case entry_or_uid + when Net::LDAP::Entry + entry = entry_or_uid + if !entry[uid_attr].empty? + entry[uid_attr].map { |uid| Net::LDAP::Filter.eq("memberUid", uid) }. + reduce(:|) + end + when String + Net::LDAP::Filter.eq("memberUid", entry_or_uid) end end diff --git a/lib/github/ldap/group.rb b/lib/github/ldap/group.rb index 25066a0..633e034 100644 --- a/lib/github/ldap/group.rb +++ b/lib/github/ldap/group.rb @@ -69,6 +69,11 @@ def member_names end end + # Internal: Returns true if the object class(es) provided match a group's. + def group?(object_class) + self.class.group?(object_class) + end + # Internal - Check if an object class includes the member names # Use `&` rathen than `include?` because both are arrays. # @@ -76,7 +81,7 @@ def member_names # will fail to match correctly unless we also downcase our group classes. # # Returns true if the object class includes one of the group class names. - def group?(object_class) + def self.group?(object_class) !(GROUP_CLASS_NAMES.map(&:downcase) & object_class.map(&:downcase)).empty? end diff --git a/lib/github/ldap/member_search.rb b/lib/github/ldap/member_search.rb new file mode 100644 index 0000000..d051268 --- /dev/null +++ b/lib/github/ldap/member_search.rb @@ -0,0 +1,4 @@ +require 'github/ldap/member_search/base' +require 'github/ldap/member_search/classic' +require 'github/ldap/member_search/recursive' +require 'github/ldap/member_search/active_directory' diff --git a/lib/github/ldap/member_search/active_directory.rb b/lib/github/ldap/member_search/active_directory.rb new file mode 100644 index 0000000..f78085a --- /dev/null +++ b/lib/github/ldap/member_search/active_directory.rb @@ -0,0 +1,60 @@ +module GitHub + class Ldap + module MemberSearch + # Look up group members using the ActiveDirectory "in chain" matching rule. + # + # The 1.2.840.113556.1.4.1941 matching rule (LDAP_MATCHING_RULE_IN_CHAIN) + # "walks the chain of ancestry in objects all the way to the root until + # it finds a match". + # Source: http://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx + # + # This means we have an efficient method of searching for group members, + # even in nested groups, performed on the server side. + class ActiveDirectory < Base + OID = "1.2.840.113556.1.4.1941" + + # Internal: The default attributes to query for. + # NOTE: We technically don't need any by default, but if we left this + # empty, we'd be querying for *all* attributes which is less ideal. + DEFAULT_ATTRS = %w(objectClass) + + # Internal: The attributes to search for. + attr_reader :attrs + + # Public: Instantiate new search strategy. + # + # - ldap: GitHub::Ldap object + # - options: Hash of options + # + # NOTE: This overrides default behavior to configure attrs`. + def initialize(ldap, options = {}) + super + @attrs = Array(options[:attrs]).concat DEFAULT_ATTRS + end + + # Public: Performs search for group members, including groups and + # members of subgroups, using ActiveDirectory's "in chain" matching + # rule. + # + # Returns Array of Net::LDAP::Entry objects. + def perform(group) + filter = member_of_in_chain_filter(group) + + # search for all members of the group, including subgroups, by + # searching "in chain". + domains.each_with_object([]) do |domain, members| + members.concat domain.search(filter: filter, attributes: attrs) + end + end + + # Internal: Constructs a member filter using the "in chain" + # extended matching rule afforded by ActiveDirectory. + # + # Returns a Net::LDAP::Filter object. + def member_of_in_chain_filter(entry) + Net::LDAP::Filter.ex("memberOf:#{OID}", entry.dn) + end + end + end + end +end diff --git a/lib/github/ldap/member_search/base.rb b/lib/github/ldap/member_search/base.rb new file mode 100644 index 0000000..e3491f1 --- /dev/null +++ b/lib/github/ldap/member_search/base.rb @@ -0,0 +1,34 @@ +module GitHub + class Ldap + module MemberSearch + class Base + + # Internal: The GitHub::Ldap object to search domains with. + attr_reader :ldap + + # Public: Instantiate new search strategy. + # + # - ldap: GitHub::Ldap object + # - options: Hash of options + def initialize(ldap, options = {}) + @ldap = ldap + @options = options + end + + # Public: Abstract: Performs search for group members. + # + # Returns Array of Net::LDAP::Entry objects. + # def perform(entry) + # end + + # Internal: Domains to search through. + # + # Returns an Array of GitHub::Ldap::Domain objects. + def domains + @domains ||= ldap.search_domains.map { |base| ldap.domain(base) } + end + private :domains + end + end + end +end diff --git a/lib/github/ldap/member_search/classic.rb b/lib/github/ldap/member_search/classic.rb new file mode 100644 index 0000000..47bb7a1 --- /dev/null +++ b/lib/github/ldap/member_search/classic.rb @@ -0,0 +1,18 @@ +module GitHub + class Ldap + module MemberSearch + # Look up group members using the existing `Group#members` and + # `Group#subgroups` API. + class Classic < Base + # Public: Performs search for group members, including groups and + # members of subgroups recursively. + # + # Returns Array of Net::LDAP::Entry objects. + def perform(group_entry) + group = ldap.load_group(group_entry) + group.members + group.subgroups + end + end + end + end +end diff --git a/lib/github/ldap/member_search/recursive.rb b/lib/github/ldap/member_search/recursive.rb new file mode 100644 index 0000000..a36aa4d --- /dev/null +++ b/lib/github/ldap/member_search/recursive.rb @@ -0,0 +1,158 @@ +module GitHub + class Ldap + module MemberSearch + # Look up group members recursively. + # + # This results in a maximum of `depth` iterations/recursions to look up + # members of a group and its subgroups. + class Recursive < Base + include Filter + + DEFAULT_MAX_DEPTH = 9 + DEFAULT_ATTRS = %w(member uniqueMember memberUid) + + # Internal: The maximum depth to search for members. + attr_reader :depth + + # Internal: The attributes to search for. + attr_reader :attrs + + # Public: Instantiate new search strategy. + # + # - ldap: GitHub::Ldap object + # - options: Hash of options + # + # NOTE: This overrides default behavior to configure `depth` and `attrs`. + def initialize(ldap, options = {}) + super + @depth = options[:depth] || DEFAULT_MAX_DEPTH + @attrs = Array(options[:attrs]).concat DEFAULT_ATTRS + end + + # Public: Performs search for group members, including groups and + # members of subgroups recursively. + # + # Returns Array of Net::LDAP::Entry objects. + def perform(group) + # track groups found + found = Hash.new + + # track all DNs searched for (so we don't repeat searches) + searched = Set.new + + # if this is a posixGroup, return members immediately (no nesting) + uids = member_uids(group) + return entries_by_uid(uids) if uids.any? + + # track group + searched << group.dn + found[group.dn] = group + + # pull out base group's member DNs + dns = member_dns(group) + + # search for base group's subgroups + groups = dns.each_with_object([]) do |dn, groups| + groups.concat find_groups_by_dn(dn) + searched << dn + end + + # track found groups + groups.each { |g| found[g.dn] = g } + + # recursively find subgroups + unless groups.empty? + depth.times do |n| + # pull out subgroups' member DNs to search through + sub_dns = groups.each_with_object([]) do |subgroup, sub_dns| + sub_dns.concat member_dns(subgroup) + end + + # filter out if already searched for + sub_dns.reject! { |dn| searched.include?(dn) } + + # give up if there's nothing else to search for + break if sub_dns.empty? + + # search for subgroups + subgroups = sub_dns.each_with_object([]) do |dn, subgroups| + subgroups.concat find_groups_by_dn(dn) + searched << dn + end + + # give up if there were no subgroups found + break if subgroups.empty? + + # track found subgroups + subgroups.each { |g| found[g.dn] = g } + + # descend another level + groups = subgroups + end + end + + # entries to return + entries = [] + + # collect all member DNs, discarding dupes and subgroup DNs + members = found.values.each_with_object([]) do |group, dns| + entries << group + dns.concat member_dns(group) + end.uniq.reject { |dn| found.key?(dn) } + + # wrap member DNs in Net::LDAP::Entry objects + entries.concat members.map! { |dn| Net::LDAP::Entry.new(dn) } + + entries + end + + # Internal: Search for Groups by DN. + # + # Given a Distinguished Name (DN) String value, find the Group entry + # that matches it. The DN may map to a `person` entry, but we want to + # filter those out. + # + # This will find zero or one entry most of the time, but it's not + # guaranteed so we account for the possibility of more. + # + # This method is intended to be used with `Array#concat` by the caller. + # + # Returns an Array of zero or more Net::LDAP::Entry objects. + def find_groups_by_dn(dn) + ldap.search \ + base: dn, + scope: Net::LDAP::SearchScope_BaseObject, + attributes: attrs, + filter: ALL_GROUPS_FILTER + end + private :find_groups_by_dn + + # Internal: Fetch entries by UID. + # + # Returns an Array of Net::LDAP::Entry objects. + def entries_by_uid(members) + filter = members.map { |uid| Net::LDAP::Filter.eq(ldap.uid, uid) }.reduce(:|) + domains.each_with_object([]) do |domain, entries| + entries.concat domain.search(filter: filter, attributes: attrs) + end.compact + end + private :entries_by_uid + + # Internal: Returns an Array of String DNs for `groupOfNames` and + # `uniqueGroupOfNames` members. + def member_dns(entry) + MEMBERSHIP_NAMES.each_with_object([]) do |attr_name, members| + members.concat entry[attr_name] + end + end + private :member_dns + + # Internal: Returns an Array of String UIDs for PosixGroups members. + def member_uids(entry) + entry["memberUid"] + end + private :member_uids + end + end + end +end diff --git a/lib/github/ldap/membership_validators.rb b/lib/github/ldap/membership_validators.rb new file mode 100644 index 0000000..c629a37 --- /dev/null +++ b/lib/github/ldap/membership_validators.rb @@ -0,0 +1,4 @@ +require 'github/ldap/membership_validators/base' +require 'github/ldap/membership_validators/classic' +require 'github/ldap/membership_validators/recursive' +require 'github/ldap/membership_validators/active_directory' diff --git a/lib/github/ldap/membership_validators/active_directory.rb b/lib/github/ldap/membership_validators/active_directory.rb new file mode 100644 index 0000000..ff4e4fc --- /dev/null +++ b/lib/github/ldap/membership_validators/active_directory.rb @@ -0,0 +1,65 @@ +module GitHub + class Ldap + module MembershipValidators + ATTRS = %w(dn) + OID = "1.2.840.113556.1.4.1941" + + # Validates membership using the ActiveDirectory "in chain" matching rule. + # + # The 1.2.840.113556.1.4.1941 matching rule (LDAP_MATCHING_RULE_IN_CHAIN) + # "walks the chain of ancestry in objects all the way to the root until + # it finds a match". + # Source: http://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx + # + # This means we have an efficient method of searching membership even in + # nested groups, performed on the server side. + class ActiveDirectory < Base + def perform(entry) + # short circuit validation if there are no groups to check against + return true if groups.empty? + + # search for the entry on the condition that the entry is a member + # of one of the groups or their subgroups. + # + # Sets the entry to the base and scopes the search to the base, + # according to the source documentation, found here: + # http://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx + # + # Use ReferralChaser to chase any potential referrals for an entry that may be owned by a different + # domain controller. + matched = referral_chaser.search \ + filter: membership_in_chain_filter(entry), + base: entry.dn, + scope: Net::LDAP::SearchScope_BaseObject, + return_referrals: true, + attributes: ATTRS + + # membership validated if entry was matched and returned as a result + # Active Directory DNs are case-insensitive + Array(matched).map { |m| m.dn.downcase }.include?(entry.dn.downcase) + end + + def referral_chaser + @referral_chaser ||= GitHub::Ldap::ReferralChaser.new(@ldap) + end + + # Internal: Constructs a membership filter using the "in chain" + # extended matching rule afforded by ActiveDirectory. + # + # Returns a Net::LDAP::Filter object. + def membership_in_chain_filter(entry) + group_dns.map do |dn| + Net::LDAP::Filter.ex("memberOf:#{OID}", dn) + end.reduce(:|) + end + + # Internal: the group DNs to check against. + # + # Returns an Array of String DNs. + def group_dns + @group_dns ||= groups.map(&:dn) + end + end + end + end +end diff --git a/lib/github/ldap/membership_validators/base.rb b/lib/github/ldap/membership_validators/base.rb new file mode 100644 index 0000000..be378d1 --- /dev/null +++ b/lib/github/ldap/membership_validators/base.rb @@ -0,0 +1,39 @@ +module GitHub + class Ldap + module MembershipValidators + class Base + + # Internal: The GitHub::Ldap object to search domains with. + attr_reader :ldap + + # Internal: an Array of Net::LDAP::Entry group objects to validate with. + attr_reader :groups + + # Public: Instantiate new validator. + # + # - ldap: GitHub::Ldap object + # - groups: Array of Net::LDAP::Entry group objects + # - options: Hash of options + def initialize(ldap, groups, options = {}) + @ldap = ldap + @groups = groups + @options = options + end + + # Abstract: Performs the membership validation check. + # + # Returns Boolean whether the entry's membership is validated or not. + # def perform(entry) + # end + + # Internal: Domains to search through. + # + # Returns an Array of GitHub::Ldap::Domain objects. + def domains + @domains ||= ldap.search_domains.map { |base| ldap.domain(base) } + end + private :domains + end + end + end +end diff --git a/lib/github/ldap/membership_validators/classic.rb b/lib/github/ldap/membership_validators/classic.rb new file mode 100644 index 0000000..7fafb3d --- /dev/null +++ b/lib/github/ldap/membership_validators/classic.rb @@ -0,0 +1,34 @@ +module GitHub + class Ldap + module MembershipValidators + # Validates membership using `GitHub::Ldap::Domain#membership`. + # + # This is a simple wrapper for existing functionality in order to expose + # it consistently with the new approach. + class Classic < Base + def perform(entry) + # short circuit validation if there are no groups to check against + return true if groups.empty? + + domains.each do |domain| + membership = domain.membership(entry, group_names) + + if !membership.empty? + entry[:groups] = membership + return true + end + end + + false + end + + # Internal: the group names to look up membership for. + # + # Returns an Array of String group names (CNs). + def group_names + @group_names ||= groups.map { |g| g[:cn].first } + end + end + end + end +end diff --git a/lib/github/ldap/membership_validators/recursive.rb b/lib/github/ldap/membership_validators/recursive.rb new file mode 100644 index 0000000..3b78545 --- /dev/null +++ b/lib/github/ldap/membership_validators/recursive.rb @@ -0,0 +1,117 @@ +module GitHub + class Ldap + module MembershipValidators + # Validates membership recursively. + # + # The first step checks whether the entry is a direct member of the given + # groups. If they are, then we've validated membership successfully. + # + # If not, query for all of the groups that have our groups as members, + # then we check if the entry is a member of any of those. + # + # This is repeated until the entry is found, recursing and requesting + # groups in bulk each iteration until we hit the maximum depth allowed + # and have to give up. + # + # This results in a maximum of `depth` queries (per domain) to validate + # membership in a list of groups. + class Recursive < Base + include Filter + + DEFAULT_MAX_DEPTH = 9 + ATTRS = %w(dn cn) + + # Internal: The maximum depth to search for membership. + attr_reader :depth + + # Public: Instantiate new search strategy. + # + # - ldap: GitHub::Ldap object + # - groups: Array of Net::LDAP::Entry group objects + # - options: Hash of options + # depth: Integer limit of recursion + # + # NOTE: This overrides default behavior to configure `depth`. + def initialize(ldap, groups, options = {}) + super + @depth = options[:depth] || DEFAULT_MAX_DEPTH + end + + def perform(entry, depth_override = nil) + if depth_override + warn "DEPRECATION WARNING: Calling Recursive#perform with a second argument is deprecated." + warn "Usage:" + warn " strategy = GitHub::Ldap::MembershipValidators::Recursive.new \\" + warn " ldap, depth: 5" + warn " strategy#perform(entry)" + end + + # short circuit validation if there are no groups to check against + return true if groups.empty? + + domains.each do |domain| + # find groups entry is an immediate member of + membership = domain.search(filter: member_filter(entry), attributes: ATTRS) + + # success if any of these groups match the restricted auth groups + return true if membership.any? { |entry| group_dns.include?(entry.dn) } + + # give up if the entry has no memberships to recurse + next if membership.empty? + + # recurse to at most `depth` + (depth_override || depth).times do |n| + # find groups whose members include membership groups + membership = domain.search(filter: membership_filter(membership), attributes: ATTRS) + + # success if any of these groups match the restricted auth groups + return true if membership.any? { |entry| group_dns.include?(entry.dn) } + + # give up if there are no more membersips to recurse + break if membership.empty? + end + + # give up on this base if there are no memberships to test + next if membership.empty? + end + + false + end + + # Internal: Construct a filter to find groups this entry is a direct + # member of. + # + # Overloads the included `GitHub::Ldap::Filters#member_filter` method + # to inject `posixGroup` handling. + # + # Returns a Net::LDAP::Filter object. + def member_filter(entry_or_uid, uid = ldap.uid) + filter = super(entry_or_uid) + + if ldap.posix_support_enabled? + if posix_filter = posix_member_filter(entry_or_uid, uid) + filter |= posix_filter + end + end + + filter + end + + # Internal: Construct a filter to find groups whose members are the + # Array of String group DNs passed in. + # + # Returns a String filter. + def membership_filter(groups) + groups.map { |entry| member_filter(entry, :cn) }.reduce(:|) + end + + # Internal: the group DNs to check against. + # + # Returns an Array of String DNs. + def group_dns + @group_dns ||= groups.map(&:dn) + end + end + end + end +end diff --git a/lib/github/ldap/referral_chaser.rb b/lib/github/ldap/referral_chaser.rb new file mode 100644 index 0000000..4811c51 --- /dev/null +++ b/lib/github/ldap/referral_chaser.rb @@ -0,0 +1,98 @@ +module GitHub + class Ldap + + # This class adds referral chasing capability to a GitHub::Ldap connection. + # + # See: https://technet.microsoft.com/en-us/library/cc978014.aspx + # http://www.umich.edu/~dirsvcs/ldap/doc/other/ldap-ref.html + # + class ReferralChaser + + # Public - Creates a ReferralChaser that decorates an instance of GitHub::Ldap + # with additional functionality to the #search method, allowing it to chase + # any referral entries and aggregate the results into a single response. + # + # connection - The instance of GitHub::Ldap to use for searching. Will use + # the connection's authentication, (admin_user and admin_password) as credentials + # for connecting to referred domain controllers. + def initialize(connection) + @connection = connection + @admin_user = connection.admin_user + @admin_password = connection.admin_password + @port = connection.port + end + + # Public - Search the domain controller represented by this instance's connection. + # If a referral is returned, search only one of the domain controllers indicated + # by the referral entries, per RFC 4511 (https://tools.ietf.org/html/rfc4511): + # + # "If the client wishes to progress the operation, it contacts one of + # the supported services found in the referral. If multiple URIs are + # present, the client assumes that any supported URI may be used to + # progress the operation." + # + # options - is a hash with the same options that Net::LDAP::Connection#search supports. + # Referral searches will use the given options, but will replace options[:base] + # with the referral URL's base search dn. + # + # Does not take a block argument as GitHub::Ldap and Net::LDAP::Connection#search do. + # + # Will not recursively follow any subsequent referrals. + # + # Returns an Array of Net::LDAP::Entry. + def search(options) + search_results = [] + referral_entries = [] + + search_results = connection.search(options) do |entry| + if entry && entry[:search_referrals] + referral_entries << entry + end + end + + unless referral_entries.empty? + entry = referral_entries.first + referral_string = entry[:search_referrals].first + if GitHub::Ldap::URL.valid?(referral_string) + referral = Referral.new(referral_string, admin_user, admin_password, port) + search_results = referral.search(options) + end + end + + Array(search_results) + end + + private + + attr_reader :connection, :admin_user, :admin_password, :port + + # Represents a referral entry from an LDAP search result. Constructs a corresponding + # GitHub::Ldap object from the paramaters on the referral_url and provides a #search + # method to continue the search on the referred domain. + class Referral + def initialize(referral_url, admin_user, admin_password, port=nil) + url = GitHub::Ldap::URL.new(referral_url) + @search_base = url.dn + + connection_options = { + host: url.host, + port: port || url.port, + scope: url.scope, + admin_user: admin_user, + admin_password: admin_password + } + + @connection = GitHub::Ldap::ConnectionCache.get_connection(connection_options) + end + + # Search the referred domain controller with options, merging in the referred search + # base DN onto options[:base]. + def search(options) + connection.search(options.merge(base: search_base)) + end + + attr_reader :search_base, :connection + end + end + end +end diff --git a/lib/github/ldap/server.rb b/lib/github/ldap/server.rb index c2cf10c..c7f624a 100644 --- a/lib/github/ldap/server.rb +++ b/lib/github/ldap/server.rb @@ -38,6 +38,8 @@ def self.start_server(options = {}) @server_options[:domain] = @server_options[:user_domain] @server_options[:tmpdir] ||= server_tmp + @server_options[:quiet] = false if @server_options[:verbose] + @ldap_server = Ladle::Server.new(@server_options) @ldap_server.start end diff --git a/lib/github/ldap/url.rb b/lib/github/ldap/url.rb new file mode 100644 index 0000000..5c733a7 --- /dev/null +++ b/lib/github/ldap/url.rb @@ -0,0 +1,87 @@ +module GitHub + class Ldap + + # This class represents an LDAP URL + # + # See: https://tools.ietf.org/html/rfc4516#section-2 + # https://docs.oracle.com/cd/E19957-01/817-6707/urls.html + # + class URL + extend Forwardable + SCOPES = { + "base" => Net::LDAP::SearchScope_BaseObject, + "one" => Net::LDAP::SearchScope_SingleLevel, + "sub" => Net::LDAP::SearchScope_WholeSubtree + } + SCOPES.default = Net::LDAP::SearchScope_BaseObject + + attr_reader :dn, :attributes, :scope, :filter + + def_delegators :@uri, :port, :host, :scheme + + # Public - Creates a new GitHub::Ldap::URL object with :port, :host and :scheme + # delegated to a URI object parsed from url_string, and then parses the + # query params according to the LDAP specification. + # + # url_string - An LDAP URL string. + # returns - a GitHub::Ldap::URL with the following attributes: + # host - Name or IP of the LDAP server. + # port - The given port, defaults to 389. + # dn - The base search DN. + # attributes - The comma-delimited list of attributes to be returned. + # scope - The scope of the search. + # filter - Search filter to apply to entries within the specified scope of the search. + # + # Supported LDAP URL strings look like this, where sections in brackets are optional: + # + # ldap[s]://[hostport][/[dn[?[attributes][?[scope][?[filter]]]]]] + # + # where: + # + # hostport is a host name with an optional ":portnumber" + # dn is the base DN to be used for an LDAP search operation + # attributes is a comma separated list of attributes to be retrieved + # scope is one of these three strings: base one sub (default=base) + # filter is LDAP search filter as used in a call to ldap_search + # + # For example: + # + # ldap://dc4.ghe.local:456/CN=Maggie,DC=dc4,DC=ghe,DC=local?cn,mail?base?(cn=Charlie) + # + def initialize(url_string) + if !self.class.valid?(url_string) + raise InvalidLdapURLException.new("Invalid LDAP URL: #{url_string}") + end + @uri = URI(url_string) + @dn = URI.unescape(@uri.path.sub(/^\//, "")) + if @uri.query + @attributes, @scope, @filter = @uri.query.split("?") + end + end + + def self.valid?(url_string) + url_string =~ URI::regexp && ["ldap", "ldaps"].include?(URI(url_string).scheme) + end + + # Maps the returned scope value from the URL to one of Net::LDAP::Scopes + # + # The URL scope value can be one of: + # "base" - retrieves information only about the DN (base_dn) specified. + # "one" - retrieves information about entries one level below the DN (base_dn) specified. The base entry is not included in this scope. + # "sub" - retrieves information about entries at all levels below the DN (base_dn) specified. The base entry is included in this scope. + # + # Which will map to one of the following Net::LDAP::Scopes: + # SearchScope_BaseObject = 0 + # SearchScope_SingleLevel = 1 + # SearchScope_WholeSubtree = 2 + # + # If no scope or an invalid scope is given, defaults to SearchScope_BaseObject + def net_ldap_scope + Net::LDAP::SearchScopes[SCOPES[scope]] + end + + class InvalidLdapURLException < Exception; end + end + end +end + diff --git a/lib/github/ldap/user_search/active_directory.rb b/lib/github/ldap/user_search/active_directory.rb new file mode 100644 index 0000000..2bec4ad --- /dev/null +++ b/lib/github/ldap/user_search/active_directory.rb @@ -0,0 +1,51 @@ +module GitHub + class Ldap + module UserSearch + class ActiveDirectory < Default + + private + + # Private - Overridden from base class to set the base to "", and use the + # Global Catalog to perform the user search. + def search(search_options) + Array(global_catalog_connection.search(search_options.merge(options))) + end + + def global_catalog_connection + GlobalCatalog.connection(ldap) + end + + # When doing a global search for a user's DN, set the search base to blank + def options + super.merge(base: "") + end + end + + class GlobalCatalog < Net::LDAP + STANDARD_GC_PORT = 3268 + LDAPS_GC_PORT = 3269 + + # Returns a connection to the Active Directory Global Catalog + # + # See: https://technet.microsoft.com/en-us/library/cc728188(v=ws.10).aspx + # + def self.connection(ldap) + @global_catalog_instance ||= begin + netldap = ldap.connection + # This is ugly, but Net::LDAP doesn't expose encryption or auth + encryption = netldap.instance_variable_get(:@encryption) + auth = netldap.instance_variable_get(:@auth) + + new({ + host: ldap.host, + instrumentation_service: ldap.instrumentation_service, + port: encryption ? LDAPS_GC_PORT : STANDARD_GC_PORT, + auth: auth, + encryption: encryption + }) + end + end + end + end + end +end diff --git a/lib/github/ldap/user_search/default.rb b/lib/github/ldap/user_search/default.rb new file mode 100644 index 0000000..2f1aa3f --- /dev/null +++ b/lib/github/ldap/user_search/default.rb @@ -0,0 +1,40 @@ +module GitHub + class Ldap + module UserSearch + # The default user search strategy, mainly for allowing Domain#user? to + # search for a user on the configured domain controller, or use the Global + # Catalog to search across the entire Active Directory forest. + class Default + include Filter + + def initialize(ldap) + @ldap = ldap + @options = { + :attributes => [], + :paged_searches_supported => true, + :size => 1 + } + end + + # Performs a normal search on the configured domain controller + # using the default base DN, uid, search_options + def perform(login, base_name, uid, search_options) + search_options[:filter] = login_filter(uid, login) + search_options[:base] = base_name + search(options.merge(search_options)) + end + + # The default search. This can be overridden by a child class + # like GitHub::Ldap::UserSearch::ActiveDirectory to change the + # scope of the search. + def search(options) + ldap.search(options) + end + + private + + attr_reader :options, :ldap + end + end + end +end diff --git a/script/changelog b/script/changelog new file mode 100755 index 0000000..8df90b0 --- /dev/null +++ b/script/changelog @@ -0,0 +1,29 @@ +#!/usr/bin/env sh +# Usage: script/changelog [-r ] [-b ] [-h ] +# +# repo: base string of GitHub repository url. e.g. "user_or_org/repository". Defaults to git remote url. +# base: git ref to compare from. e.g. "v1.3.1". Defaults to latest git tag. +# head: git ref to compare to. Defaults to "HEAD". +# +# Generate a changelog preview from pull requests merged between `base` and +# `head`. +# +set -e + +[ $# -eq 0 ] && set -- --help + +# parse args +repo=$(git remote -v | grep push | awk '{print $2}' | cut -d'/' -f4- | sed 's/\.git//') +base=$(git tag -l | sort -n | tail -n 1) +head="HEAD" +api_url="https://api.github.com" + +echo "# $repo $base..$head" +echo + +# get merged PR's. Better way is to query the API for these, but this is easier +for pr in $(git log --oneline $base..$head | grep "Merge pull request" | awk '{gsub("#",""); print $5}') +do + # frustrated with trying to pull out the right values, fell back to ruby + 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)]})"' +done diff --git a/script/cibuild-apacheds b/script/cibuild-apacheds new file mode 100755 index 0000000..6e02fa0 --- /dev/null +++ b/script/cibuild-apacheds @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +set -e +set -x + +cd `dirname $0`/.. + +bundle exec rake diff --git a/script/cibuild-openldap b/script/cibuild-openldap new file mode 100755 index 0000000..6e02fa0 --- /dev/null +++ b/script/cibuild-openldap @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +set -e +set -x + +cd `dirname $0`/.. + +bundle exec rake diff --git a/script/install-openldap b/script/install-openldap new file mode 100755 index 0000000..2deddad --- /dev/null +++ b/script/install-openldap @@ -0,0 +1,42 @@ +#!/usr/bin/env sh +set -e +set -x + +BASE_PATH="$( cd `dirname $0`/../test/fixtures/openldap && pwd )" +SEED_PATH="$( cd `dirname $0`/../test/fixtures/common && pwd )" + +DEBIAN_FRONTEND=noninteractive sudo -E apt-get install -y --force-yes slapd time ldap-utils + +sudo /etc/init.d/slapd stop + +TMPDIR=$(mktemp -d) +cd $TMPDIR + +# Delete data and reconfigure. +sudo rm -rf /etc/ldap/slapd.d/* +sudo rm -rf /var/lib/ldap/* +sudo slapadd -F /etc/ldap/slapd.d -b "cn=config" -l $BASE_PATH/slapd.conf.ldif +# Load memberof and ref-int overlays and configure them. +sudo slapadd -F /etc/ldap/slapd.d -b "cn=config" -l $BASE_PATH/memberof.ldif + +# Add base domain. +sudo slapadd -F /etc/ldap/slapd.d < "ad1.ghe.dev")) + conn2 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad1.ghe.dev")) + assert_equal conn1.object_id, conn2.object_id + end + + def test_creates_new_connections_per_host + conn1 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad1.ghe.dev")) + conn2 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad2.ghe.dev")) + conn3 = GitHub::Ldap::ConnectionCache.get_connection(options.merge(:host => "ad2.ghe.dev")) + refute_equal conn1.object_id, conn2.object_id + assert_equal conn2.object_id, conn3.object_id + end +end diff --git a/test/domain_test.rb b/test/domain_test.rb index 470e00d..4fc0dee 100644 --- a/test/domain_test.rb +++ b/test/domain_test.rb @@ -7,13 +7,13 @@ def setup end def test_user_valid_login - user = @domain.valid_login?('calavera', 'passworD1') - assert_equal 'uid=calavera,dc=github,dc=com', user.dn + assert user = @domain.valid_login?('user1', 'passworD1') + assert_equal 'uid=user1,ou=People,dc=github,dc=com', user.dn end def test_user_with_invalid_password - assert !@domain.valid_login?('calavera', 'foo'), - "Login `calavera` expected to be invalid with password `foo`" + assert !@domain.valid_login?('user1', 'foo'), + "Login `user1` expected to be invalid with password `foo`" end def test_user_with_invalid_login @@ -22,115 +22,132 @@ def test_user_with_invalid_login end def test_groups_in_server - assert_equal 2, @domain.groups(%w(Enterprise People)).size + assert_equal 2, @domain.groups(%w(ghe-users ghe-admins)).size end def test_user_in_group - user = @domain.valid_login?('calavera', 'passworD1') + assert user = @domain.valid_login?('user1', 'passworD1') - assert @domain.is_member?(user, %w(Enterprise People)), - "Expected `Enterprise` or `Poeple` to include the member `#{user.dn}`" + assert @domain.is_member?(user, %w(ghe-users ghe-admins)), + "Expected `ghe-users` or `ghe-admins` to include the member `#{user.dn}`" end def test_user_not_in_different_group - user = @domain.valid_login?('calavera', 'passworD1') + user = @domain.valid_login?('user1', 'passworD1') - assert !@domain.is_member?(user, %w(People)), - "Expected `Poeple` not to include the member `#{user.dn}`" + refute @domain.is_member?(user, %w(ghe-admins)), + "Expected `ghe-admins` not to include the member `#{user.dn}`" end def test_user_without_group - user = @domain.valid_login?('ldaptest', 'secret') + user = @domain.valid_login?('groupless-user1', 'passworD1') - assert !@domain.is_member?(user, %w(People)), - "Expected `People` not to include the member `#{user.dn}`" + assert !@domain.is_member?(user, %w(all-users)), + "Expected `all-users` not to include the member `#{user.dn}`" end - def test_authenticate_doesnt_return_invalid_users - user = @domain.authenticate!('calavera', 'passworD1') - assert_equal 'uid=calavera,dc=github,dc=com', user.dn + def test_authenticate_returns_valid_users + user = @domain.authenticate!('user1', 'passworD1') + assert_equal 'uid=user1,ou=People,dc=github,dc=com', user.dn end def test_authenticate_doesnt_return_invalid_users - assert !@domain.authenticate!('calavera', 'foo'), + refute @domain.authenticate!('user1', 'foo'), "Expected `authenticate!` to not return an invalid user" end def test_authenticate_check_valid_user_and_groups - user = @domain.authenticate!('calavera', 'passworD1', %w(Enterprise People)) + user = @domain.authenticate!('user1', 'passworD1', %w(ghe-users ghe-admins)) - assert_equal 'uid=calavera,dc=github,dc=com', user.dn + assert_equal 'uid=user1,ou=People,dc=github,dc=com', user.dn end def test_authenticate_doesnt_return_valid_users_in_different_groups - assert !@domain.authenticate!('calavera', 'passworD1', %w(People)), + refute @domain.authenticate!('user1', 'passworD1', %w(ghe-admins)), "Expected `authenticate!` to not return an user" end def test_membership_empty_for_non_members - user = @ldap.domain('uid=calavera,dc=github,dc=com').bind + user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind - assert @domain.membership(user, %w(People)).empty?, - "Expected `calavera` not to be a member of `People`." + assert @domain.membership(user, %w(ghe-admins)).empty?, + "Expected `user1` not to be a member of `ghe-admins`." end def test_membership_groups_for_members - user = @ldap.domain('uid=calavera,dc=github,dc=com').bind - groups = @domain.membership(user, %w(Enterprise People)) + user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind + groups = @domain.membership(user, %w(ghe-users ghe-admins)) assert_equal 1, groups.size - assert_equal 'cn=Enterprise,ou=Group,dc=github,dc=com', groups.first.dn + assert_equal 'cn=ghe-users,ou=Groups,dc=github,dc=com', groups.first.dn end def test_membership_with_virtual_attributes ldap = GitHub::Ldap.new(options.merge(virtual_attributes: true)) - user = ldap.domain('uid=calavera,dc=github,dc=com').bind - user[:memberof] = 'cn=Enterprise,ou=Group,dc=github,dc=com' + + user = ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind + user[:memberof] = 'cn=ghe-admins,ou=Groups,dc=github,dc=com' domain = ldap.domain("dc=github,dc=com") - groups = domain.membership(user, %w(Enterprise People)) + groups = domain.membership(user, %w(ghe-admins)) assert_equal 1, groups.size - assert_equal 'cn=Enterprise,ou=Group,dc=github,dc=com', groups.first.dn + assert_equal 'cn=ghe-admins,ou=Groups,dc=github,dc=com', groups.first.dn end def test_search assert 1, @domain.search( attributes: %w(uid), - filter: Net::LDAP::Filter.eq('uid', 'calavera')).size + filter: Net::LDAP::Filter.eq('uid', 'user1')).size end def test_search_override_base_name assert 1, @domain.search( base: "this base name is incorrect", attributes: %w(uid), - filter: Net::LDAP::Filter.eq('uid', 'calavera')).size + filter: Net::LDAP::Filter.eq('uid', 'user1')).size end def test_user_exists - assert_equal 'uid=calavera,dc=github,dc=com', @domain.user?('calavera').dn + assert user = @domain.user?('user1') + assert_equal 'uid=user1,ou=People,dc=github,dc=com', user.dn end def test_user_wildcards_are_filtered - assert !@domain.user?('cal*'), 'Expected uid `cal*` to not complete' + refute @domain.user?('user*'), 'Expected uid `user*` to not complete' end def test_user_does_not_exist - assert !@domain.user?('foobar'), 'Expected uid `foobar` to not exist.' + refute @domain.user?('foobar'), 'Expected uid `foobar` to not exist.' end def test_user_returns_every_attribute - assert_equal ['calavera@github.com'], @domain.user?('calavera')[:mail] + assert user = @domain.user?('user1') + assert_equal ['user1@github.com'], user[:mail] + end + + def test_user_returns_subset_of_attributes + assert entry = @domain.user?('user1', :attributes => [:cn]) + assert_equal [:dn, :cn], entry.attribute_names end def test_auth_binds - user = @domain.user?('calavera') - assert @domain.auth(user, 'passworD1'), 'Expected user to be bound.' + assert user = @domain.user?('user1') + assert @domain.auth(user, 'passworD1'), 'Expected user to bind' end def test_auth_does_not_bind - user = @domain.user?('calavera') - assert !@domain.auth(user, 'foo'), 'Expected user not to be bound.' + assert user = @domain.user?('user1') + refute @domain.auth(user, 'foo'), 'Expected user not not bind' + end + + def test_user_search_returns_first_entry + entry = mock("Net::Ldap::Entry") + search_strategy = mock("GitHub::Ldap::UserSearch::Default") + search_strategy.stubs(:perform).returns([entry]) + @ldap.expects(:user_search_strategy).returns(search_strategy) + user = @domain.user?('user1', :attributes => [:cn]) + assert_equal entry, user end end @@ -143,48 +160,37 @@ class GitHubLdapDomainUnauthenticatedTest < GitHub::Ldap::UnauthenticatedTest end class GitHubLdapDomainNestedGroupsTest < GitHub::Ldap::Test - def self.test_server_options - {user_fixtures: FIXTURES.join('github-with-subgroups.ldif').to_s} - end - def setup @ldap = GitHub::Ldap.new(options) @domain = @ldap.domain("dc=github,dc=com") end def test_membership_in_subgroups - user = @ldap.domain('uid=rubiojr,ou=users,dc=github,dc=com').bind + user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind - assert @domain.is_member?(user, %w(enterprise-ops)), - "Expected `enterprise-ops` to include the member `#{user.dn}`" + assert @domain.is_member?(user, %w(nested-groups)), + "Expected `nested-groups` to include the member `#{user.dn}`" end def test_membership_in_deeply_nested_subgroups - assert user = @ldap.domain('uid=user1.1.1.1,ou=users,dc=github,dc=com').bind + assert user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind - assert @domain.is_member?(user, %w(group1)), - "Expected `group1` to include the member `#{user.dn}` via deep recursion" + assert @domain.is_member?(user, %w(n-depth-nested-group4)), + "Expected `n-depth-nested-group4` to include the member `#{user.dn}` via deep recursion" end end class GitHubLdapPosixGroupsWithRecursionFallbackTest < GitHub::Ldap::Test - def self.test_server_options - { - custom_schemas: FIXTURES.join('posixGroup.schema.ldif'), - user_fixtures: FIXTURES.join('github-with-posixGroups.ldif').to_s, - # so we exercise the recursive group search fallback - recursive_group_search_fallback: true - } - end - def setup - @ldap = GitHub::Ldap.new(options) + opts = options.merge \ + recursive_group_search_fallback: true + @ldap = GitHub::Ldap.new(opts) @domain = @ldap.domain("dc=github,dc=com") - @cn = "enterprise-posix-devs" + @cn = "posix-group1" end def test_membership_for_posixGroups - assert user = @ldap.domain('uid=mtodd,ou=users,dc=github,dc=com').bind + assert user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind assert @domain.is_member?(user, [@cn]), "Expected `#{@cn}` to include the member `#{user.dn}`" @@ -192,23 +198,16 @@ def test_membership_for_posixGroups end class GitHubLdapPosixGroupsWithoutRecursionTest < GitHub::Ldap::Test - def self.test_server_options - { - custom_schemas: FIXTURES.join('posixGroup.schema.ldif'), - user_fixtures: FIXTURES.join('github-with-posixGroups.ldif').to_s, - # so we test the test the non-recursive group membership search - recursive_group_search_fallback: false - } - end - def setup - @ldap = GitHub::Ldap.new(options) + opts = options.merge \ + recursive_group_search_fallback: false + @ldap = GitHub::Ldap.new(opts) @domain = @ldap.domain("dc=github,dc=com") - @cn = "enterprise-posix-devs" + @cn = "posix-group1" end def test_membership_for_posixGroups - assert user = @ldap.domain('uid=mtodd,ou=users,dc=github,dc=com').bind + assert user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind assert @domain.is_member?(user, [@cn]), "Expected `#{@cn}` to include the member `#{user.dn}`" @@ -218,27 +217,32 @@ def test_membership_for_posixGroups # Specifically testing that this doesn't break when posixGroups are not # supported. class GitHubLdapWithoutPosixGroupsTest < GitHub::Ldap::Test - def self.test_server_options - { - custom_schemas: FIXTURES.join('posixGroup.schema.ldif'), - user_fixtures: FIXTURES.join('github-with-posixGroups.ldif').to_s, - # so we test the test the non-recursive group membership search - recursive_group_search_fallback: false, - # explicitly disable posixGroup support (even if the schema supports it) - posix_support: false - } - end - def setup - @ldap = GitHub::Ldap.new(options) + opts = options.merge \ + recursive_group_search_fallback: false, # test non-recursive group membership search + posix_support: false # disable posixGroup support + @ldap = GitHub::Ldap.new(opts) @domain = @ldap.domain("dc=github,dc=com") - @cn = "enterprise-posix-devs" + @cn = "posix-group1" end def test_membership_for_posixGroups - assert user = @ldap.domain('uid=mtodd,ou=users,dc=github,dc=com').bind + assert user = @ldap.domain('uid=user1,ou=People,dc=github,dc=com').bind refute @domain.is_member?(user, [@cn]), "Expected `#{@cn}` to not include the member `#{user.dn}`" end end + +class GitHubLdapActiveDirectoryGroupsTest < GitHub::Ldap::Test + def run(*) + return super if self.class.test_env == "activedirectory" + Minitest::Result.from(self) + end + + def test_filter_groups + domain = GitHub::Ldap.new(options).domain("DC=ad,DC=ghe,DC=local") + results = domain.filter_groups("ghe-admins") + assert_equal 1, results.size + end +end diff --git a/test/filter_test.rb b/test/filter_test.rb index 8fc6ba2..4da83c9 100644 --- a/test/filter_test.rb +++ b/test/filter_test.rb @@ -1,6 +1,6 @@ require_relative 'test_helper' -class FilterTest < Minitest::Test +class FilterTest < GitHub::Ldap::Test class Subject include GitHub::Ldap::Filter def initialize(ldap) @@ -16,11 +16,12 @@ def [](field) end def setup - @ldap = GitHub::Ldap.new(:uid => 'uid') + @ldap = GitHub::Ldap.new(options.merge(:uid => 'uid')) @subject = Subject.new(@ldap) @me = 'uid=calavera,dc=github,dc=com' @uid = "calavera" - @entry = Entry.new(@me, @uid) + @entry = Net::LDAP::Entry.new(@me) + @entry[:uid] = @uid end def test_member_present @@ -32,6 +33,11 @@ def test_member_equal @subject.member_filter(@entry).to_s end + def test_member_equal_with_string + assert_equal "(|(member=#{@me})(uniqueMember=#{@me}))", + @subject.member_filter(@entry.dn).to_s + end + def test_posix_member_without_uid @entry.uid = nil assert_nil @subject.posix_member_filter(@entry, @ldap.uid) @@ -42,6 +48,11 @@ def test_posix_member_equal @subject.posix_member_filter(@entry, @ldap.uid).to_s end + def test_posix_member_equal_string + assert_equal "(memberUid=#{@uid})", + @subject.posix_member_filter(@uid).to_s + end + def test_groups_reduced assert_equal "(|(cn=Enterprise)(cn=People))", @subject.group_filter(%w(Enterprise People)).to_s diff --git a/test/fixtures/common/seed.ldif b/test/fixtures/common/seed.ldif new file mode 100644 index 0000000..29284bb --- /dev/null +++ b/test/fixtures/common/seed.ldif @@ -0,0 +1,369 @@ +dn: ou=People,dc=github,dc=com +objectClass: top +objectClass: organizationalUnit +ou: People + +dn: ou=Groups,dc=github,dc=com +objectClass: top +objectClass: organizationalUnit +ou: Groups + +# Directory Superuser +dn: uid=admin,dc=github,dc=com +uid: admin +cn: system administrator +sn: administrator +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +displayName: Directory Superuser +userPassword: passworD1 + +# Users 1-10 + +dn: uid=user1,ou=People,dc=github,dc=com +uid: user1 +cn: user1 +sn: user1 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user1@github.com + +dn: uid=user2,ou=People,dc=github,dc=com +uid: user2 +cn: user2 +sn: user2 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user2@github.com + +dn: uid=user3,ou=People,dc=github,dc=com +uid: user3 +cn: user3 +sn: user3 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user3@github.com + +dn: uid=user4,ou=People,dc=github,dc=com +uid: user4 +cn: user4 +sn: user4 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user4@github.com + +dn: uid=user5,ou=People,dc=github,dc=com +uid: user5 +cn: user5 +sn: user5 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user5@github.com + +dn: uid=user6,ou=People,dc=github,dc=com +uid: user6 +cn: user6 +sn: user6 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user6@github.com + +dn: uid=user7,ou=People,dc=github,dc=com +uid: user7 +cn: user7 +sn: user7 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user7@github.com + +dn: uid=user8,ou=People,dc=github,dc=com +uid: user8 +cn: user8 +sn: user8 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user8@github.com + +dn: uid=user9,ou=People,dc=github,dc=com +uid: user9 +cn: user9 +sn: user9 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user9@github.com + +dn: uid=user10,ou=People,dc=github,dc=com +uid: user10 +cn: user10 +sn: user10 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user10@github.com + +# Emailless User + +dn: uid=emailless-user1,ou=People,dc=github,dc=com +uid: emailless-user1 +cn: emailless-user1 +sn: emailless-user1 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 + +# Groupless User + +dn: uid=groupless-user1,ou=People,dc=github,dc=com +uid: groupless-user1 +cn: groupless-user1 +sn: groupless-user1 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 + +# Admin User + +dn: uid=admin1,ou=People,dc=github,dc=com +uid: admin1 +cn: admin1 +sn: admin1 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: admin1@github.com + +# Groups + +dn: cn=ghe-users,ou=Groups,dc=github,dc=com +cn: ghe-users +objectClass: groupOfNames +member: uid=user1,ou=People,dc=github,dc=com +member: uid=emailless-user1,ou=People,dc=github,dc=com + +dn: cn=all-users,ou=Groups,dc=github,dc=com +cn: all-users +objectClass: groupOfNames +member: cn=ghe-users,ou=Groups,dc=github,dc=com +member: uid=user1,ou=People,dc=github,dc=com +member: uid=user2,ou=People,dc=github,dc=com +member: uid=user3,ou=People,dc=github,dc=com +member: uid=user4,ou=People,dc=github,dc=com +member: uid=user5,ou=People,dc=github,dc=com +member: uid=user6,ou=People,dc=github,dc=com +member: uid=user7,ou=People,dc=github,dc=com +member: uid=user8,ou=People,dc=github,dc=com +member: uid=user9,ou=People,dc=github,dc=com +member: uid=user10,ou=People,dc=github,dc=com +member: uid=emailless-user1,ou=People,dc=github,dc=com + +dn: cn=ghe-admins,ou=Groups,dc=github,dc=com +cn: ghe-admins +objectClass: groupOfNames +member: uid=admin1,ou=People,dc=github,dc=com + +dn: cn=all-admins,ou=Groups,dc=github,dc=com +cn: all-admins +objectClass: groupOfNames +member: cn=ghe-admins,ou=Groups,dc=github,dc=com +member: uid=admin1,ou=People,dc=github,dc=com + +dn: cn=n-member-group10,ou=Groups,dc=github,dc=com +cn: n-member-group10 +objectClass: groupOfNames +member: uid=user1,ou=People,dc=github,dc=com +member: uid=user2,ou=People,dc=github,dc=com +member: uid=user3,ou=People,dc=github,dc=com +member: uid=user4,ou=People,dc=github,dc=com +member: uid=user5,ou=People,dc=github,dc=com +member: uid=user6,ou=People,dc=github,dc=com +member: uid=user7,ou=People,dc=github,dc=com +member: uid=user8,ou=People,dc=github,dc=com +member: uid=user9,ou=People,dc=github,dc=com +member: uid=user10,ou=People,dc=github,dc=com + +dn: cn=nested-group1,ou=Groups,dc=github,dc=com +cn: nested-group1 +objectClass: groupOfNames +member: uid=user1,ou=People,dc=github,dc=com +member: uid=user2,ou=People,dc=github,dc=com +member: uid=user3,ou=People,dc=github,dc=com +member: uid=user4,ou=People,dc=github,dc=com +member: uid=user5,ou=People,dc=github,dc=com +member: uid=user6,ou=People,dc=github,dc=com +member: uid=user7,ou=People,dc=github,dc=com +member: uid=user8,ou=People,dc=github,dc=com +member: uid=user9,ou=People,dc=github,dc=com +member: uid=user10,ou=People,dc=github,dc=com + +dn: cn=nested-groups,ou=Groups,dc=github,dc=com +cn: nested-groups +objectClass: groupOfNames +member: cn=nested-group1,ou=Groups,dc=github,dc=com + +dn: cn=n-member-nested-group1,ou=Groups,dc=github,dc=com +cn: n-member-nested-group1 +objectClass: groupOfNames +member: cn=nested-group1,ou=Groups,dc=github,dc=com + +dn: cn=deeply-nested-group0.0.0,ou=Groups,dc=github,dc=com +cn: deeply-nested-group0.0.0 +objectClass: groupOfNames +member: uid=user1,ou=People,dc=github,dc=com +member: uid=user2,ou=People,dc=github,dc=com +member: uid=user3,ou=People,dc=github,dc=com +member: uid=user4,ou=People,dc=github,dc=com +member: uid=user5,ou=People,dc=github,dc=com + +dn: cn=deeply-nested-group0.0.1,ou=Groups,dc=github,dc=com +cn: deeply-nested-group0.0.1 +objectClass: groupOfNames +member: uid=user6,ou=People,dc=github,dc=com +member: uid=user7,ou=People,dc=github,dc=com +member: uid=user8,ou=People,dc=github,dc=com +member: uid=user9,ou=People,dc=github,dc=com +member: uid=user10,ou=People,dc=github,dc=com + +dn: cn=deeply-nested-group0.0,ou=Groups,dc=github,dc=com +cn: deeply-nested-group0.0 +objectClass: groupOfNames +member: cn=deeply-nested-group0.0.0,ou=Groups,dc=github,dc=com +member: cn=deeply-nested-group0.0.1,ou=Groups,dc=github,dc=com + +dn: cn=deeply-nested-group0,ou=Groups,dc=github,dc=com +cn: deeply-nested-group0 +objectClass: groupOfNames +member: cn=deeply-nested-group0.0,ou=Groups,dc=github,dc=com + +dn: cn=deeply-nested-groups,ou=Groups,dc=github,dc=com +cn: deeply-nested-groups +objectClass: groupOfNames +member: cn=deeply-nested-group0,ou=Groups,dc=github,dc=com + +dn: cn=n-depth-nested-group1,ou=Groups,dc=github,dc=com +cn: n-depth-nested-group1 +objectClass: groupOfNames +member: cn=nested-group1,ou=Groups,dc=github,dc=com + +dn: cn=n-depth-nested-group2,ou=Groups,dc=github,dc=com +cn: n-depth-nested-group2 +objectClass: groupOfNames +member: cn=n-depth-nested-group1,ou=Groups,dc=github,dc=com + +dn: cn=n-depth-nested-group3,ou=Groups,dc=github,dc=com +cn: n-depth-nested-group3 +objectClass: groupOfNames +member: cn=n-depth-nested-group2,ou=Groups,dc=github,dc=com + +dn: cn=n-depth-nested-group4,ou=Groups,dc=github,dc=com +cn: n-depth-nested-group4 +objectClass: groupOfNames +member: cn=n-depth-nested-group3,ou=Groups,dc=github,dc=com + +dn: cn=n-depth-nested-group5,ou=Groups,dc=github,dc=com +cn: n-depth-nested-group5 +objectClass: groupOfNames +member: cn=n-depth-nested-group4,ou=Groups,dc=github,dc=com + +dn: cn=n-depth-nested-group6,ou=Groups,dc=github,dc=com +cn: n-depth-nested-group6 +objectClass: groupOfNames +member: cn=n-depth-nested-group5,ou=Groups,dc=github,dc=com + +dn: cn=n-depth-nested-group7,ou=Groups,dc=github,dc=com +cn: n-depth-nested-group7 +objectClass: groupOfNames +member: cn=n-depth-nested-group6,ou=Groups,dc=github,dc=com + +dn: cn=n-depth-nested-group8,ou=Groups,dc=github,dc=com +cn: n-depth-nested-group8 +objectClass: groupOfNames +member: cn=n-depth-nested-group7,ou=Groups,dc=github,dc=com + +dn: cn=n-depth-nested-group9,ou=Groups,dc=github,dc=com +cn: n-depth-nested-group9 +objectClass: groupOfNames +member: cn=n-depth-nested-group8,ou=Groups,dc=github,dc=com + +dn: cn=head-group,ou=Groups,dc=github,dc=com +cn: head-group +objectClass: groupOfNames +member: cn=tail-group,ou=Groups,dc=github,dc=com +member: uid=user1,ou=People,dc=github,dc=com +member: uid=user2,ou=People,dc=github,dc=com +member: uid=user3,ou=People,dc=github,dc=com +member: uid=user4,ou=People,dc=github,dc=com +member: uid=user5,ou=People,dc=github,dc=com + +dn: cn=tail-group,ou=Groups,dc=github,dc=com +cn: tail-group +objectClass: groupOfNames +member: cn=head-group,ou=Groups,dc=github,dc=com +member: uid=user6,ou=People,dc=github,dc=com +member: uid=user7,ou=People,dc=github,dc=com +member: uid=user8,ou=People,dc=github,dc=com +member: uid=user9,ou=People,dc=github,dc=com +member: uid=user10,ou=People,dc=github,dc=com + +dn: cn=recursively-nested-groups,ou=Groups,dc=github,dc=com +cn: recursively-nested-groups +objectClass: groupOfNames +member: cn=head-group,ou=Groups,dc=github,dc=com +member: cn=tail-group,ou=Groups,dc=github,dc=com + +# posixGroup + +dn: cn=posix-group1,ou=Groups,dc=github,dc=com +cn: posix-group1 +objectClass: posixGroup +gidNumber: 1001 +memberUid: user1 +memberUid: user2 +memberUid: user3 +memberUid: user4 +memberUid: user5 + +# missing members + +dn: cn=missing-users,ou=Groups,dc=github,dc=com +cn: missing-users +objectClass: groupOfNames +member: uid=user1,ou=People,dc=github,dc=com +member: uid=user2,ou=People,dc=github,dc=com +member: uid=nonexistent-user,ou=People,dc=github,dc=com diff --git a/test/fixtures/github-with-looped-subgroups.ldif b/test/fixtures/github-with-looped-subgroups.ldif deleted file mode 100644 index 02868fe..0000000 --- a/test/fixtures/github-with-looped-subgroups.ldif +++ /dev/null @@ -1,82 +0,0 @@ -version: 1 - -# Admin user - -dn: uid=admin,dc=github,dc=com -objectClass: top -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -cn: system administrator -sn: administrator -displayName: Directory Superuser -uid: admin -userPassword: secret - -# Groups - -dn: ou=groups,dc=github,dc=com -objectclass: organizationalUnit - -dn: cn=enterprise,ou=groups,dc=github,dc=com -cn: Enterprise -objectClass: groupOfNames -member: uid=calavera,ou=users,dc=github,dc=com -member: cn=enterprise-devs,ou=groups,dc=github,dc=com -member: cn=enterprise-ops,ou=groups,dc=github,dc=com - -dn: cn=enterprise-devs,ou=groups,dc=github,dc=com -cn: enterprise-devs -objectClass: groupOfNames -member: uid=benburkert,ou=users,dc=github,dc=com -member: cn=enterprise,ou=groups,dc=github,dc=com - -dn: cn=enterprise-ops,ou=groups,dc=github,dc=com -cn: enterprise-ops -objectClass: groupOfNames -member: uid=sbryant,ou=users,dc=github,dc=com -member: cn=spaniards,ou=groups,dc=github,dc=com - -dn: cn=spaniards,ou=groups,dc=github,dc=com -cn: spaniards -objectClass: groupOfNames -member: uid=calavera,ou=users,dc=github,dc=com -member: uid=rubiojr,ou=users,dc=github,dc=com - -# Users - -dn: ou=users,dc=github,dc=com -objectclass: organizationalUnit - -dn: uid=calavera,ou=users,dc=github,dc=com -cn: David Calavera -cn: David -sn: Calavera -uid: calavera -userPassword: passworD1 -mail: calavera@github.com -objectClass: inetOrgPerson - -dn: uid=benburkert,ou=users,dc=github,dc=com -cn: benburkert -sn: benburkert -uid: benburkert -userPassword: passworD1 -mail: benburkert@github.com -objectClass: inetOrgPerson - -dn: uid=sbryant,ou=users,dc=github,dc=com -cn: sbryant -sn: sbryant -uid: sbryant -userPassword: passworD1 -mail: sbryant@github.com -objectClass: inetOrgPerson - -dn: uid=rubiojr,ou=users,dc=github,dc=com -cn: rubiojr -sn: rubiojr -uid: rubiojr -userPassword: passworD1 -mail: rubiojr@github.com -objectClass: inetOrgPerson diff --git a/test/fixtures/github-with-missing-entries.ldif b/test/fixtures/github-with-missing-entries.ldif deleted file mode 100644 index be8d316..0000000 --- a/test/fixtures/github-with-missing-entries.ldif +++ /dev/null @@ -1,85 +0,0 @@ -version: 1 - -# Admin user - -dn: uid=admin,dc=github,dc=com -objectClass: top -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -cn: system administrator -sn: administrator -displayName: Directory Superuser -uid: admin -userPassword: secret - -# Groups - -dn: ou=groups,dc=github,dc=com -objectclass: organizationalUnit - -dn: cn=enterprise,ou=groups,dc=github,dc=com -cn: Enterprise -objectClass: groupOfNames -member: uid=calavera,ou=users,dc=github,dc=com -member: cn=enterprise-devs,ou=groups,dc=github,dc=com -member: cn=enterprise-ops,ou=groups,dc=github,dc=com - -dn: cn=enterprise-devs,ou=groups,dc=github,dc=com -cn: enterprise-devs -objectClass: groupOfNames -member: uid=benburkert,ou=users,dc=github,dc=com -member: cn=enterprise,ou=groups,dc=github,dc=com - -dn: cn=enterprise-ops,ou=groups,dc=github,dc=com -cn: enterprise-ops -objectClass: groupOfNames -member: uid=sbryant,ou=users,dc=github,dc=com -member: cn=spaniards,ou=groups,dc=github,dc=com - -# The last member of this group is missing on purpose. -# See: https://github.com/github/github-ldap/pull/18 -dn: cn=spaniards,ou=groups,dc=github,dc=com -cn: spaniards -objectClass: groupOfNames -member: uid=calavera,ou=users,dc=github,dc=com -member: uid=rubiojr,ou=users,dc=github,dc=com -member: uid=felipe,ou=users,dc=github,dc=com - -# Users - -dn: ou=users,dc=github,dc=com -objectclass: organizationalUnit - -dn: uid=calavera,ou=users,dc=github,dc=com -cn: David Calavera -cn: David -sn: Calavera -uid: calavera -userPassword: passworD1 -mail: calavera@github.com -objectClass: inetOrgPerson - -dn: uid=benburkert,ou=users,dc=github,dc=com -cn: benburkert -sn: benburkert -uid: benburkert -userPassword: passworD1 -mail: benburkert@github.com -objectClass: inetOrgPerson - -dn: uid=sbryant,ou=users,dc=github,dc=com -cn: sbryant -sn: sbryant -uid: sbryant -userPassword: passworD1 -mail: sbryant@github.com -objectClass: inetOrgPerson - -dn: uid=rubiojr,ou=users,dc=github,dc=com -cn: rubiojr -sn: rubiojr -uid: rubiojr -userPassword: passworD1 -mail: rubiojr@github.com -objectClass: inetOrgPerson diff --git a/test/fixtures/github-with-posixGroups.ldif b/test/fixtures/github-with-posixGroups.ldif deleted file mode 100644 index ac8b3a0..0000000 --- a/test/fixtures/github-with-posixGroups.ldif +++ /dev/null @@ -1,50 +0,0 @@ -version: 1 - -# Admin user - -dn: uid=admin,dc=github,dc=com -objectClass: top -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -cn: system administrator -sn: administrator -displayName: Directory Superuser -uid: admin -userPassword: secret - -# Groups - -dn: ou=groups,dc=github,dc=com -objectclass: organizationalUnit -ou: groups - -# Posix Groups - -dn: cn=enterprise-posix-devs,ou=groups,dc=github,dc=com -cn: enterprise-posix-devs -objectClass: posixGroup -memberUid: benburkert -memberUid: mtodd - -# Users - -dn: ou=users,dc=github,dc=com -objectclass: organizationalUnit -ou: users - -dn: uid=benburkert,ou=users,dc=github,dc=com -cn: benburkert -sn: benburkert -uid: benburkert -userPassword: passworD1 -mail: benburkert@github.com -objectClass: inetOrgPerson - -dn: uid=mtodd,ou=users,dc=github,dc=com -cn: mtodd -sn: mtodd -uid: mtodd -userPassword: passworD1 -mail: mtodd@github.com -objectClass: inetOrgPerson diff --git a/test/fixtures/github-with-subgroups.ldif b/test/fixtures/github-with-subgroups.ldif deleted file mode 100644 index 00dc929..0000000 --- a/test/fixtures/github-with-subgroups.ldif +++ /dev/null @@ -1,146 +0,0 @@ -version: 1 - -# Admin user - -dn: uid=admin,dc=github,dc=com -objectClass: top -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -cn: system administrator -sn: administrator -displayName: Directory Superuser -uid: admin -userPassword: secret - -# Groups - -dn: ou=groups,dc=github,dc=com -objectclass: organizationalUnit -ou: groups - -dn: cn=enterprise,ou=groups,dc=github,dc=com -cn: Enterprise -objectClass: groupOfNames -member: uid=calavera,ou=users,dc=github,dc=com -member: cn=enterprise-devs,ou=groups,dc=github,dc=com -member: cn=enterprise-ops,ou=groups,dc=github,dc=com - -dn: cn=enterprise-devs,ou=groups,dc=github,dc=com -cn: enterprise-devs -objectClass: groupOfNames -member: uid=benburkert,ou=users,dc=github,dc=com - -dn: cn=enterprise-ops,ou=groups,dc=github,dc=com -cn: enterprise-ops -objectClass: groupOfNames -member: uid=sbryant,ou=users,dc=github,dc=com -member: cn=spaniards,ou=groups,dc=github,dc=com - -dn: cn=spaniards,ou=groups,dc=github,dc=com -cn: spaniards -objectClass: groupOfNames -member: uid=calavera,ou=users,dc=github,dc=com -member: uid=rubiojr,ou=users,dc=github,dc=com - -dn: cn=group1,ou=groups,dc=github,dc=com -cn: group1 -objectClass: groupOfNames -member: uid=user1,ou=users,dc=github,dc=com -member: cn=group1.1,ou=groups,dc=github,dc=com - -dn: cn=group1.1,ou=groups,dc=github,dc=com -cn: group1 -objectClass: groupOfNames -member: uid=user1.1,ou=users,dc=github,dc=com -member: cn=group1.1.1,ou=groups,dc=github,dc=com - -dn: cn=group1.1.1,ou=groups,dc=github,dc=com -cn: group1 -objectClass: groupOfNames -member: uid=user1.1.1,ou=users,dc=github,dc=com -member: cn=group1.1.1.1,ou=groups,dc=github,dc=com - -dn: cn=group1.1.1.1,ou=groups,dc=github,dc=com -cn: group1 -objectClass: groupOfNames -member: uid=user1.1.1.1,ou=users,dc=github,dc=com - -# Users - -dn: ou=users,dc=github,dc=com -objectclass: organizationalUnit -ou: users - -dn: uid=calavera,ou=users,dc=github,dc=com -cn: David Calavera -cn: David -sn: Calavera -uid: calavera -userPassword: passworD1 -mail: calavera@github.com -objectClass: inetOrgPerson - -dn: uid=benburkert,ou=users,dc=github,dc=com -cn: benburkert -sn: benburkert -uid: benburkert -userPassword: passworD1 -mail: benburkert@github.com -objectClass: inetOrgPerson - -dn: uid=sbryant,ou=users,dc=github,dc=com -cn: sbryant -sn: sbryant -uid: sbryant -userPassword: passworD1 -mail: sbryant@github.com -objectClass: inetOrgPerson - -dn: uid=rubiojr,ou=users,dc=github,dc=com -cn: rubiojr -sn: rubiojr -uid: rubiojr -userPassword: passworD1 -mail: rubiojr@github.com -objectClass: inetOrgPerson - -dn: uid=mtodd,ou=users,dc=github,dc=com -cn: mtodd -sn: mtodd -uid: mtodd -userPassword: passworD1 -mail: mtodd@github.com -objectClass: inetOrgPerson - -dn: uid=user1,ou=users,dc=github,dc=com -uid: user1 -sn: user1 -cn: user1 -userPassword: passworD1 -mail: user1@github.com -objectClass: inetOrgPerson - -dn: uid=user1.1,ou=users,dc=github,dc=com -uid: user1.1 -sn: user1.1 -cn: user1.1 -userPassword: passworD1 -mail: user1.1@github.com -objectClass: inetOrgPerson - -dn: uid=user1.1.1,ou=users,dc=github,dc=com -uid: user1.1.1 -sn: user1.1.1 -cn: user1.1.1 -userPassword: passworD1 -mail: user1.1.1@github.com -objectClass: inetOrgPerson - -dn: uid=user1.1.1.1,ou=users,dc=github,dc=com -uid: user1.1.1.1 -sn: user1.1.1.1 -cn: user1.1.1.1 -userPassword: passworD1 -mail: user1.1.1.1@github.com -objectClass: inetOrgPerson diff --git a/test/fixtures/openldap/memberof.ldif b/test/fixtures/openldap/memberof.ldif new file mode 100644 index 0000000..dac7c6b --- /dev/null +++ b/test/fixtures/openldap/memberof.ldif @@ -0,0 +1,33 @@ +dn: cn=module,cn=config +cn: module +objectClass: olcModuleList +objectClass: top +olcModulePath: /usr/lib/ldap +olcModuleLoad: memberof.la + +dn: olcOverlay={0}memberof,olcDatabase={1}hdb,cn=config +objectClass: olcConfig +objectClass: olcMemberOf +objectClass: olcOverlayConfig +objectClass: top +olcOverlay: memberof +olcMemberOfDangling: ignore +olcMemberOfRefInt: TRUE +olcMemberOfGroupOC: groupOfNames +olcMemberOfMemberAD: member +olcMemberOfMemberOfAD: memberOf + +dn: cn=module,cn=config +cn: module +objectclass: olcModuleList +objectclass: top +olcmoduleload: refint.la +olcmodulepath: /usr/lib/ldap + +dn: olcOverlay={1}refint,olcDatabase={1}hdb,cn=config +objectClass: olcConfig +objectClass: olcOverlayConfig +objectClass: olcRefintConfig +objectClass: top +olcOverlay: {1}refint +olcRefintAttribute: memberof member manager owner diff --git a/test/fixtures/openldap/slapd.conf.ldif b/test/fixtures/openldap/slapd.conf.ldif new file mode 100644 index 0000000..7d88769 --- /dev/null +++ b/test/fixtures/openldap/slapd.conf.ldif @@ -0,0 +1,67 @@ +dn: cn=config +objectClass: olcGlobal +cn: config +olcPidFile: /var/run/slapd/slapd.pid +olcArgsFile: /var/run/slapd/slapd.args +olcLogLevel: none +olcToolThreads: 1 + +dn: olcDatabase={-1}frontend,cn=config +objectClass: olcDatabaseConfig +objectClass: olcFrontendConfig +olcDatabase: {-1}frontend +olcSizeLimit: 500 +olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break +olcAccess: {1}to dn.exact="" by * read +olcAccess: {2}to dn.base="cn=Subschema" by * read + +dn: olcDatabase=config,cn=config +objectClass: olcDatabaseConfig +olcDatabase: config +olcAccess: to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break + +dn: cn=schema,cn=config +objectClass: olcSchemaConfig +cn: schema + +include: file:///etc/ldap/schema/core.ldif +include: file:///etc/ldap/schema/cosine.ldif +include: file:///etc/ldap/schema/nis.ldif +include: file:///etc/ldap/schema/inetorgperson.ldif + +dn: cn=module{0},cn=config +objectClass: olcModuleList +cn: module{0} +olcModulePath: /usr/lib/ldap +olcModuleLoad: back_hdb + +dn: olcBackend=hdb,cn=config +objectClass: olcBackendConfig +olcBackend: hdb + +dn: olcDatabase=hdb,cn=config +objectClass: olcDatabaseConfig +objectClass: olcHdbConfig +olcDatabase: hdb +olcDbCheckpoint: 512 30 +olcDbConfig: set_cachesize 1 0 0 +olcDbConfig: set_lk_max_objects 1500 +olcDbConfig: set_lk_max_locks 1500 +olcDbConfig: set_lk_max_lockers 1500 +olcLastMod: TRUE +olcSuffix: dc=github,dc=com +olcDbDirectory: /var/lib/ldap +olcRootDN: cn=admin,dc=github,dc=com +# admin's password: "passworD1" +olcRootPW: {SHA}LFSkM9eegU6j3PeGG7UuHrT/KZM= +olcDbIndex: objectClass eq +olcAccess: to attrs=userPassword,shadowLastChange + by self write + by anonymous auth + by dn="cn=admin,dc=github,dc=com" write + by * none +olcAccess: to dn.base="" by * read +olcAccess: to * + by self write + by dn="cn=admin,dc=github,dc=com" write + by * read diff --git a/test/fixtures/posixGroup.schema.ldif b/test/fixtures/posixGroup.schema.ldif index 94dd488..3ba04e0 100644 --- a/test/fixtures/posixGroup.schema.ldif +++ b/test/fixtures/posixGroup.schema.ldif @@ -1,26 +1,52 @@ version: 1 -dn: m-oid=1.3.6.1.4.1.18055.0.4.1.2.1001,ou=attributeTypes,cn=other,ou=schema +# attributetype ( 1.3.6.1.1.1.1.1 NAME 'gidNumber' +# DESC 'An integer uniquely identifying a group in an administrative domain' +# EQUALITY integerMatch +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +dn: m-oid=1.3.6.1.1.1.1.1,ou=attributeTypes,cn=other,ou=schema +objectClass: metaAttributeType +objectClass: metaTop +objectClass: top +m-collective: FALSE +m-description: An integer uniquely identifying a group in an administrative domain +m-equality: integerMatch +m-name: gidNumber +m-syntax: 1.3.6.1.4.1.1466.115.121.1.27 +m-usage: USER_APPLICATIONS +m-oid: 1.3.6.1.1.1.1.1 + +# attributetype ( 1.3.6.1.1.1.1.12 NAME 'memberUid' +# EQUALITY caseExactIA5Match +# SUBSTR caseExactIA5SubstringsMatch +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +dn: m-oid=1.3.6.1.1.1.1.12,ou=attributeTypes,cn=other,ou=schema objectClass: metaAttributeType objectClass: metaTop objectClass: top m-collective: FALSE m-description: memberUid -m-equality: caseExactMatch +m-equality: caseExactIA5Match m-name: memberUid -m-syntax: 1.3.6.1.4.1.1466.115.121.1.15 +m-syntax: 1.3.6.1.4.1.1466.115.121.1.26 m-usage: USER_APPLICATIONS -m-oid: 1.3.6.1.4.1.18055.0.4.1.2.1001 +m-oid: 1.3.6.1.1.1.1.12 -dn: m-oid=1.3.6.1.4.1.18055.0.4.1.3.1001,ou=objectClasses,cn=other,ou=schema +# objectclass ( 1.3.6.1.1.1.2.2 NAME 'posixGroup' SUP top STRUCTURAL +# DESC 'Abstraction of a group of accounts' +# MUST ( cn $ gidNumber ) +# MAY ( userPassword $ memberUid $ description ) ) +dn: m-oid=1.3.6.1.1.1.2.2,ou=objectClasses,cn=other,ou=schema objectClass: metaObjectClass objectClass: metaTop objectClass: top m-description: posixGroup -m-may: cn -m-may: sn +m-must: cn +m-must: gidNumber m-may: memberUid +m-may: userPassword +m-may: description m-supobjectclass: top m-name: posixGroup -m-oid: 1.3.6.1.4.1.18055.0.4.1.3.1001 +m-oid: 1.3.6.1.1.1.2.2 m-typeobjectclass: STRUCTURAL diff --git a/test/group_test.rb b/test/group_test.rb index 6f1714d..1ed5f82 100644 --- a/test/group_test.rb +++ b/test/group_test.rb @@ -1,17 +1,13 @@ require_relative 'test_helper' class GitHubLdapGroupTest < GitHub::Ldap::Test - def self.test_server_options - {user_fixtures: FIXTURES.join('github-with-subgroups.ldif').to_s} - end - def groups_domain - @ldap.domain("ou=groups,dc=github,dc=com") + @ldap.domain("ou=Groups,dc=github,dc=com") end def setup @ldap = GitHub::Ldap.new(options) - @group = @ldap.group("cn=enterprise,ou=groups,dc=github,dc=com") + @group = @ldap.group("cn=ghe-users,ou=Groups,dc=github,dc=com") end def test_group? @@ -25,34 +21,36 @@ def test_group? end def test_subgroups - assert_equal 3, @group.subgroups.size + group = @ldap.group("cn=deeply-nested-group0.0,ou=Groups,dc=github,dc=com") + assert_equal 2, group.subgroups.size end def test_members_from_subgroups - assert_equal 4, @group.members.size + group = @ldap.group("cn=deeply-nested-group0.0,ou=Groups,dc=github,dc=com") + assert_equal 10, group.members.size end def test_all_domain_groups groups = groups_domain.all_groups - assert_equal 8, groups.size + assert_equal 27, groups.size end def test_filter_domain_groups - groups = groups_domain.filter_groups('devs') + groups = groups_domain.filter_groups('ghe-users') assert_equal 1, groups.size end def test_filter_domain_groups_limited groups = [] - groups_domain.filter_groups('enter', size: 1) do |entry| + groups_domain.filter_groups('deeply-nested-group', size: 1) do |entry| groups << entry end assert_equal 1, groups.size end def test_filter_domain_groups_unlimited - groups = groups_domain.filter_groups('ent') - assert_equal 3, groups.size + groups = groups_domain.filter_groups('deeply-nested-group') + assert_equal 5, groups.size end def test_unknown_group @@ -62,33 +60,25 @@ def test_unknown_group end class GitHubLdapLoopedGroupTest < GitHub::Ldap::Test - def self.test_server_options - {user_fixtures: FIXTURES.join('github-with-looped-subgroups.ldif').to_s} - end - def setup - @group = GitHub::Ldap.new(options).group("cn=enterprise,ou=groups,dc=github,dc=com") + @group = GitHub::Ldap.new(options).group("cn=recursively-nested-groups,ou=Groups,dc=github,dc=com") end def test_members_from_subgroups - assert_equal 4, @group.members.size + assert_equal 10, @group.members.size end end class GitHubLdapMissingEntriesTest < GitHub::Ldap::Test - def self.test_server_options - {user_fixtures: FIXTURES.join('github-with-missing-entries.ldif').to_s} - end - def setup @ldap = GitHub::Ldap.new(options) end def test_load_right_members - assert_equal 3, @ldap.domain("cn=spaniards,ou=groups,dc=github,dc=com").bind[:member].size + assert_equal 3, @ldap.domain("cn=missing-users,ou=groups,dc=github,dc=com").bind[:member].size end def test_ignore_missing_member_entries - assert_equal 2, @ldap.group("cn=spaniards,ou=groups,dc=github,dc=com").members.size + assert_equal 2, @ldap.group("cn=missing-users,ou=groups,dc=github,dc=com").members.size end end diff --git a/test/ldap_test.rb b/test/ldap_test.rb index 27861d3..d5e9297 100644 --- a/test/ldap_test.rb +++ b/test/ldap_test.rb @@ -9,74 +9,153 @@ def test_connection_with_default_options assert @ldap.test_connection, "Ldap connection expected to succeed" end + def test_connection_with_list_of_hosts_with_one_valid_host + ldap = GitHub::Ldap.new(options.merge(hosts: [["localhost", options[:port]]])) + assert ldap.test_connection, "Ldap connection expected to succeed" + end + + def test_connection_with_list_of_hosts_with_first_valid + ldap = GitHub::Ldap.new(options.merge(hosts: [["localhost", options[:port]], ["invalid.local", options[:port]]])) + assert ldap.test_connection, "Ldap connection expected to succeed" + end + + def test_connection_with_list_of_hosts_with_first_invalid + ldap = GitHub::Ldap.new(options.merge(hosts: [["invalid.local", options[:port]], ["localhost", options[:port]]])) + assert ldap.test_connection, "Ldap connection expected to succeed" + end + def test_simple_tls - assert_equal :simple_tls, @ldap.check_encryption(:ssl) - assert_equal :simple_tls, @ldap.check_encryption('SSL') - assert_equal :simple_tls, @ldap.check_encryption(:simple_tls) + expected = { method: :simple_tls, tls_options: { } } + assert_equal expected, @ldap.check_encryption(:ssl) + assert_equal expected, @ldap.check_encryption('SSL') + assert_equal expected, @ldap.check_encryption(:simple_tls) end def test_start_tls - assert_equal :start_tls, @ldap.check_encryption(:tls) - assert_equal :start_tls, @ldap.check_encryption('TLS') - assert_equal :start_tls, @ldap.check_encryption(:start_tls) + expected = { method: :start_tls, tls_options: { } } + assert_equal expected, @ldap.check_encryption(:tls) + assert_equal expected, @ldap.check_encryption('TLS') + assert_equal expected, @ldap.check_encryption(:start_tls) + end + + def test_tls_validation + assert_equal({ method: :start_tls, tls_options: { verify_mode: OpenSSL::SSL::VERIFY_PEER } }, + @ldap.check_encryption(:tls, verify_mode: OpenSSL::SSL::VERIFY_PEER)) + assert_equal({ method: :start_tls, tls_options: { verify_mode: OpenSSL::SSL::VERIFY_NONE } }, + @ldap.check_encryption(:tls, verify_mode: OpenSSL::SSL::VERIFY_NONE)) + assert_equal({ method: :start_tls, tls_options: { cert_store: "some/path" } }, + @ldap.check_encryption(:tls, cert_store: "some/path")) + assert_equal({ method: :start_tls, tls_options: {} }, + @ldap.check_encryption(:tls, nil)) end def test_search_delegator - @ldap.domain('dc=github,dc=com').valid_login? 'calavera', 'secret' + assert user = @ldap.domain('dc=github,dc=com').valid_login?('user1', 'passworD1') - result = @ldap.search( - {:base => 'dc=github,dc=com', - :attributes => %w(uid), - :filter => Net::LDAP::Filter.eq('uid', 'calavera')}) + result = @ldap.search \ + :base => 'dc=github,dc=com', + :attributes => %w(uid), + :filter => Net::LDAP::Filter.eq('uid', 'user1') refute result.empty? - assert_equal 'calavera', result.first[:uid].first + assert_equal 'user1', result.first[:uid].first end - def test_virtual_attributes_defaults - @ldap = GitHub::Ldap.new(options.merge(virtual_attributes: true)) - - assert @ldap.virtual_attributes.enabled?, "Expected to have virtual attributes enabled with defaults" - assert_equal 'memberOf', @ldap.virtual_attributes.virtual_membership + def test_virtual_attributes_disabled + refute @ldap.virtual_attributes.enabled?, "Expected to have virtual attributes disabled" end - def test_virtual_attributes_defaults + def test_virtual_attributes_configured ldap = GitHub::Ldap.new(options.merge(virtual_attributes: true)) - assert ldap.virtual_attributes.enabled?, "Expected to have virtual attributes enabled with defaults" + assert ldap.virtual_attributes.enabled?, + "Expected virtual attributes to be enabled" assert_equal 'memberOf', ldap.virtual_attributes.virtual_membership end - def test_virtual_attributes_hash + def test_virtual_attributes_configured_with_membership_attribute ldap = GitHub::Ldap.new(options.merge(virtual_attributes: {virtual_membership: "isMemberOf"})) - assert ldap.virtual_attributes.enabled?, "Expected to have virtual attributes enabled with defaults" + assert ldap.virtual_attributes.enabled?, + "Expected virtual attributes to be enabled" assert_equal 'isMemberOf', ldap.virtual_attributes.virtual_membership end - def test_virtual_attributes_disabled - refute @ldap.virtual_attributes.enabled?, "Expected to have virtual attributes disabled" - end - def test_search_domains ldap = GitHub::Ldap.new(options.merge(search_domains: ['dc=github,dc=com'])) - result = ldap.search(filter: Net::LDAP::Filter.eq('uid', 'calavera')) + result = ldap.search(filter: Net::LDAP::Filter.eq('uid', 'user1')) refute result.empty? - assert_equal 'calavera', result.first[:uid].first + assert_equal 'user1', result.first[:uid].first end def test_instruments_search events = @service.subscribe "search.github_ldap" - result = @ldap.search(filter: "(uid=calavera)", :base => "dc=github,dc=com") + result = @ldap.search(filter: "(uid=user1)", :base => "dc=github,dc=com") refute_predicate result, :empty? payload, event_result = events.pop assert payload assert event_result assert_equal result, event_result - assert_equal "(uid=calavera)", payload[:filter].to_s + assert_equal "(uid=user1)", payload[:filter].to_s assert_equal "dc=github,dc=com", payload[:base] end + + def test_search_strategy_defaults + assert_equal GitHub::Ldap::MembershipValidators::Recursive, @ldap.membership_validator + assert_equal GitHub::Ldap::MemberSearch::Recursive, @ldap.member_search_strategy + end + + def test_search_strategy_detects_active_directory + caps = Net::LDAP::Entry.new + caps[:supportedcapabilities] = [GitHub::Ldap::ACTIVE_DIRECTORY_V51_OID] + + @ldap.stub :capabilities, caps do + @ldap.configure_search_strategy :detect + + assert_equal GitHub::Ldap::MembershipValidators::ActiveDirectory, @ldap.membership_validator + assert_equal GitHub::Ldap::MemberSearch::ActiveDirectory, @ldap.member_search_strategy + end + end + + def test_search_strategy_configured_to_classic + @ldap.configure_search_strategy :classic + assert_equal GitHub::Ldap::MembershipValidators::Classic, @ldap.membership_validator + assert_equal GitHub::Ldap::MemberSearch::Classic, @ldap.member_search_strategy + end + + def test_search_strategy_configured_to_recursive + @ldap.configure_search_strategy :recursive + assert_equal GitHub::Ldap::MembershipValidators::Recursive, @ldap.membership_validator + assert_equal GitHub::Ldap::MemberSearch::Recursive, @ldap.member_search_strategy + end + + def test_search_strategy_configured_to_active_directory + @ldap.configure_search_strategy :active_directory + assert_equal GitHub::Ldap::MembershipValidators::ActiveDirectory, @ldap.membership_validator + assert_equal GitHub::Ldap::MemberSearch::ActiveDirectory, @ldap.member_search_strategy + end + + def test_search_strategy_misconfigured_to_unrecognized_strategy_falls_back_to_default + @ldap.configure_search_strategy :unknown + assert_equal GitHub::Ldap::MembershipValidators::Recursive, @ldap.membership_validator + assert_equal GitHub::Ldap::MemberSearch::Recursive, @ldap.member_search_strategy + end + + def test_user_search_strategy_global_catalog_when_configured + @ldap.configure_user_search_strategy("global_catalog") + assert_kind_of GitHub::Ldap::UserSearch::ActiveDirectory, @ldap.user_search_strategy + end + + def test_user_search_strategy_default_when_configured + @ldap.configure_user_search_strategy("default") + refute_kind_of GitHub::Ldap::UserSearch::ActiveDirectory, @ldap.user_search_strategy + assert_kind_of GitHub::Ldap::UserSearch::Default, @ldap.user_search_strategy + end + + def test_capabilities + assert_kind_of Net::LDAP::Entry, @ldap.capabilities + end end class GitHubLdapTest < GitHub::Ldap::Test diff --git a/test/member_search/active_directory_test.rb b/test/member_search/active_directory_test.rb new file mode 100644 index 0000000..19f2c96 --- /dev/null +++ b/test/member_search/active_directory_test.rb @@ -0,0 +1,79 @@ +require_relative '../test_helper' + +class GitHubLdapActiveDirectoryMemberSearchStubbedTest < GitHub::Ldap::Test + # Only run when AD integration tests aren't run + def run(*) + return super if self.class.test_env != "activedirectory" + Minitest::Result.from(self) + end + + def find_group(cn) + @domain.groups([cn]).first + end + + def setup + @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com))) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('user1') + @strategy = GitHub::Ldap::MemberSearch::ActiveDirectory.new(@ldap) + end + + def test_finds_group_members + members = + @ldap.stub :search, [@entry] do + @strategy.perform(find_group("nested-group1")).map(&:dn) + end + assert_includes members, @entry.dn + end + + def test_finds_nested_group_members + members = + @ldap.stub :search, [@entry] do + @strategy.perform(find_group("n-depth-nested-group1")).map(&:dn) + end + assert_includes members, @entry.dn + end + + def test_finds_deeply_nested_group_members + members = + @ldap.stub :search, [@entry] do + @strategy.perform(find_group("n-depth-nested-group9")).map(&:dn) + end + assert_includes members, @entry.dn + end +end + +# See test/support/vm/activedirectory/README.md for details +class GitHubLdapActiveDirectoryMemberSearchIntegrationTest < GitHub::Ldap::Test + # Only run this test suite if ActiveDirectory is configured + def run(*) + return super if self.class.test_env == "activedirectory" + Minitest::Result.from(self) + end + + def find_group(cn) + @domain.groups([cn]).first + end + + def setup + @ldap = GitHub::Ldap.new(options) + @domain = @ldap.domain(options[:search_domains]) + @entry = @domain.user?('user1') + @strategy = GitHub::Ldap::MemberSearch::ActiveDirectory.new(@ldap) + end + + def test_finds_group_members + members = @strategy.perform(find_group("nested-group1")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_finds_nested_group_members + members = @strategy.perform(find_group("n-depth-nested-group1")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_finds_deeply_nested_group_members + members = @strategy.perform(find_group("n-depth-nested-group9")).map(&:dn) + assert_includes members, @entry.dn + end +end diff --git a/test/member_search/classic_test.rb b/test/member_search/classic_test.rb new file mode 100644 index 0000000..656e12b --- /dev/null +++ b/test/member_search/classic_test.rb @@ -0,0 +1,40 @@ +require_relative '../test_helper' + +class GitHubLdapRecursiveMemberSearchTest < GitHub::Ldap::Test + def setup + @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com))) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('user1') + @strategy = GitHub::Ldap::MemberSearch::Classic.new(@ldap) + end + + def find_group(cn) + @domain.groups([cn]).first + end + + def test_finds_group_members + members = @strategy.perform(find_group("nested-group1")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_finds_nested_group_members + members = @strategy.perform(find_group("n-depth-nested-group1")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_finds_deeply_nested_group_members + members = @strategy.perform(find_group("n-depth-nested-group9")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_finds_posix_group_members + members = @strategy.perform(find_group("posix-group1")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_does_not_respect_configured_depth_limit + strategy = GitHub::Ldap::MemberSearch::Classic.new(@ldap, depth: 2) + members = strategy.perform(find_group("n-depth-nested-group9")).map(&:dn) + assert_includes members, @entry.dn + end +end diff --git a/test/member_search/recursive_test.rb b/test/member_search/recursive_test.rb new file mode 100644 index 0000000..a2d388d --- /dev/null +++ b/test/member_search/recursive_test.rb @@ -0,0 +1,40 @@ +require_relative '../test_helper' + +class GitHubLdapRecursiveMemberSearchTest < GitHub::Ldap::Test + def setup + @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com))) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('user1') + @strategy = GitHub::Ldap::MemberSearch::Recursive.new(@ldap) + end + + def find_group(cn) + @domain.groups([cn]).first + end + + def test_finds_group_members + members = @strategy.perform(find_group("nested-group1")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_finds_nested_group_members + members = @strategy.perform(find_group("n-depth-nested-group1")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_finds_deeply_nested_group_members + members = @strategy.perform(find_group("n-depth-nested-group9")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_finds_posix_group_members + members = @strategy.perform(find_group("posix-group1")).map(&:dn) + assert_includes members, @entry.dn + end + + def test_respects_configured_depth_limit + strategy = GitHub::Ldap::MemberSearch::Recursive.new(@ldap, depth: 2) + members = strategy.perform(find_group("n-depth-nested-group9")).map(&:dn) + refute_includes members, @entry.dn + end +end diff --git a/test/membership_validators/active_directory_test.rb b/test/membership_validators/active_directory_test.rb new file mode 100644 index 0000000..2160f8d --- /dev/null +++ b/test/membership_validators/active_directory_test.rb @@ -0,0 +1,137 @@ +require_relative '../test_helper' + +class GitHubLdapActiveDirectoryMembershipValidatorsStubbedTest < GitHub::Ldap::Test + # Only run when AD integration tests aren't run + def run(*) + return super if self.class.test_env != "activedirectory" + Minitest::Result.from(self) + end + + def setup + @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com))) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('user1') + @validator = GitHub::Ldap::MembershipValidators::ActiveDirectory + end + + def make_validator(groups) + groups = @domain.groups(groups) + @validator.new(@ldap, groups) + end + + def test_validates_user_in_group + validator = make_validator(%w(nested-group1)) + + @ldap.stub :search, [@entry] do + assert validator.perform(@entry) + end + end + + def test_validates_user_in_child_group + validator = make_validator(%w(n-depth-nested-group1)) + + @ldap.stub :search, [@entry] do + assert validator.perform(@entry) + end + end + + def test_validates_user_in_grandchild_group + validator = make_validator(%w(n-depth-nested-group2)) + + @ldap.stub :search, [@entry] do + assert validator.perform(@entry) + end + end + + def test_validates_user_in_great_grandchild_group + validator = make_validator(%w(n-depth-nested-group3)) + + @ldap.stub :search, [@entry] do + assert validator.perform(@entry) + end + end + + def test_does_not_validate_user_not_in_group + validator = make_validator(%w(ghe-admins)) + + @ldap.stub :search, [] do + refute validator.perform(@entry) + end + end + + def test_does_not_validate_user_not_in_any_group + entry = @domain.user?('groupless-user1') + validator = make_validator(%w(all-users)) + + @ldap.stub :search, [] do + refute validator.perform(entry) + end + end +end + +# See test/support/vm/activedirectory/README.md for details +class GitHubLdapActiveDirectoryMembershipValidatorsIntegrationTest < GitHub::Ldap::Test + # Only run this test suite if ActiveDirectory is configured + def run(*) + return super if self.class.test_env == "activedirectory" + Minitest::Result.from(self) + end + + def setup + @ldap = GitHub::Ldap.new(options) + @domain = @ldap.domain(options[:search_domains]) + @entry = @domain.user?('user1') + @validator = GitHub::Ldap::MembershipValidators::ActiveDirectory + end + + def make_validator(groups) + groups = @domain.groups(groups) + @validator.new(@ldap, groups) + end + + def test_validates_user_in_group + validator = make_validator(%w(nested-group1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_child_group + validator = make_validator(%w(n-depth-nested-group1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_grandchild_group + validator = make_validator(%w(n-depth-nested-group2)) + assert validator.perform(@entry) + end + + def test_validates_user_in_great_grandchild_group + validator = make_validator(%w(n-depth-nested-group3)) + assert validator.perform(@entry) + end + + def test_does_not_validate_user_not_in_group + validator = make_validator(%w(ghe-admins)) + refute validator.perform(@entry) + end + + def test_does_not_validate_user_not_in_any_group + skip "update AD ldif to have a groupless user" + @entry = @domain.user?('groupless-user1') + validator = make_validator(%w(all-users)) + refute validator.perform(@entry) + end + + def test_validates_user_in_posix_group + validator = make_validator(%w(posix-group1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_group_with_differently_cased_dn + validator = make_validator(%w(all-users)) + @entry[:dn].map(&:upcase!) + assert validator.perform(@entry) + + @entry[:dn].map(&:downcase!) + assert validator.perform(@entry) + end +end diff --git a/test/membership_validators/classic_test.rb b/test/membership_validators/classic_test.rb new file mode 100644 index 0000000..9b2c32b --- /dev/null +++ b/test/membership_validators/classic_test.rb @@ -0,0 +1,51 @@ +require_relative '../test_helper' + +class GitHubLdapClassicMembershipValidatorsTest < GitHub::Ldap::Test + def setup + @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com))) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('user1') + @validator = GitHub::Ldap::MembershipValidators::Classic + end + + def make_validator(groups) + groups = @domain.groups(groups) + @validator.new(@ldap, groups) + end + + def test_validates_user_in_group + validator = make_validator(%w(nested-group1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_child_group + validator = make_validator(%w(n-depth-nested-group1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_grandchild_group + validator = make_validator(%w(n-depth-nested-group2)) + assert validator.perform(@entry) + end + + def test_validates_user_in_great_grandchild_group + validator = make_validator(%w(n-depth-nested-group3)) + assert validator.perform(@entry) + end + + def test_does_not_validate_user_not_in_group + validator = make_validator(%w(ghe-admins)) + refute validator.perform(@entry) + end + + def test_does_not_validate_user_not_in_any_group + @entry = @domain.user?('groupless-user1') + validator = make_validator(%w(all-users)) + refute validator.perform(@entry) + end + + def test_validates_user_in_posix_group + validator = make_validator(%w(posix-group1)) + assert validator.perform(@entry) + end +end diff --git a/test/membership_validators/recursive_test.rb b/test/membership_validators/recursive_test.rb new file mode 100644 index 0000000..072ffca --- /dev/null +++ b/test/membership_validators/recursive_test.rb @@ -0,0 +1,56 @@ +require_relative '../test_helper' + +class GitHubLdapRecursiveMembershipValidatorsTest < GitHub::Ldap::Test + def setup + @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com))) + @domain = @ldap.domain("dc=github,dc=com") + @entry = @domain.user?('user1') + @validator = GitHub::Ldap::MembershipValidators::Recursive + end + + def make_validator(groups, options = {}) + groups = @domain.groups(groups) + @validator.new(@ldap, groups, options) + end + + def test_validates_user_in_group + validator = make_validator(%w(nested-group1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_child_group + validator = make_validator(%w(n-depth-nested-group1)) + assert validator.perform(@entry) + end + + def test_validates_user_in_grandchild_group + validator = make_validator(%w(n-depth-nested-group2)) + assert validator.perform(@entry) + end + + def test_validates_user_in_great_grandchild_group + validator = make_validator(%w(n-depth-nested-group3)) + assert validator.perform(@entry) + end + + def test_does_not_validate_user_in_great_granchild_group_with_depth + validator = make_validator(%w(n-depth-nested-group3), depth: 2) + refute validator.perform(@entry) + end + + def test_does_not_validate_user_not_in_group + validator = make_validator(%w(ghe-admins)) + refute validator.perform(@entry) + end + + def test_does_not_validate_user_not_in_any_group + @entry = @domain.user?('groupless-user1') + validator = make_validator(%w(all-users)) + refute validator.perform(@entry) + end + + def test_validates_user_in_posix_group + validator = make_validator(%w(posix-group1)) + assert validator.perform(@entry) + end +end diff --git a/test/posix_group_test.rb b/test/posix_group_test.rb index a71e252..e21b3ac 100644 --- a/test/posix_group_test.rb +++ b/test/posix_group_test.rb @@ -1,54 +1,51 @@ require_relative 'test_helper' class GitHubLdapPosixGroupTest < GitHub::Ldap::Test - def self.test_server_options - {user_fixtures: FIXTURES.join('github-with-subgroups.ldif').to_s} - end - def setup @simple_group = Net::LDAP::Entry._load(""" -dn: cn=enterprise-posix-devs,ou=groups,dc=github,dc=com -cn: enterprise-posix-devs +dn: cn=simple-group,ou=Groups,dc=github,dc=com +cn: simple-group objectClass: posixGroup -memberUid: benburkert -memberUid: mtodd""") +memberUid: user1 +memberUid: user2""") @one_level_deep_group = Net::LDAP::Entry._load(""" -dn: cn=enterprise-posix-ops,ou=groups,dc=github,dc=com -cn: enterprise-posix-ops +dn: cn=one-level-deep-group,ou=Groups,dc=github,dc=com +cn: one-level-deep-group objectClass: posixGroup objectClass: groupOfNames -memberUid: sbryant -member: cn=spaniards,ou=groups,dc=github,dc=com""") +memberUid: user6 +member: cn=ghe-users,ou=Groups,dc=github,dc=com""") @two_levels_deep_group = Net::LDAP::Entry._load(""" -dn: cn=enterprise-posix,ou=groups,dc=github,dc=com -cn: Enterprise Posix +dn: cn=two-levels-deep-group,ou=Groups,dc=github,dc=com +cn: two-levels-deep-group objectClass: posixGroup objectClass: groupOfNames -memberUid: calavera -member: cn=enterprise-devs,ou=groups,dc=github,dc=com -member: cn=enterprise-ops,ou=groups,dc=github,dc=com""") +memberUid: user6 +member: cn=n-depth-nested-group2,ou=Groups,dc=github,dc=com +member: cn=posix-group1,ou=Groups,dc=github,dc=com""") @empty_group = Net::LDAP::Entry._load(""" -dn: cn=enterprise-posix-empty,ou=groups,dc=github,dc=com -cn: enterprise-posix-empty +dn: cn=empty-group,ou=Groups,dc=github,dc=com +cn: empty-group objectClass: posixGroup""") @ldap = GitHub::Ldap.new(options.merge(search_domains: %w(dc=github,dc=com))) end def test_posix_group - assert GitHub::Ldap::PosixGroup.valid?(@simple_group), + entry = @ldap.search(filter: "(cn=posix-group1)").first + assert GitHub::Ldap::PosixGroup.valid?(entry), "Expected entry to be a valid posixGroup" end def test_posix_simple_members - group = GitHub::Ldap::PosixGroup.new(@ldap, @simple_group) + assert group = @ldap.group("cn=posix-group1,ou=Groups,dc=github,dc=com") members = group.members - assert_equal 2, members.size - assert_equal %w(benburkert mtodd), members.map(&:uid).flatten.sort + assert_equal 5, members.size + assert_equal %w(user1 user2 user3 user4 user5), members.map(&:uid).flatten.sort end def test_posix_combined_group @@ -62,7 +59,7 @@ def test_posix_combined_group_unique_members group = GitHub::Ldap::PosixGroup.new(@ldap, @two_levels_deep_group) members = group.members - assert_equal 4, members.size + assert_equal 10, members.size end def test_empty_subgroups @@ -81,7 +78,7 @@ def test_posix_combined_group_subgroups def test_is_member_simple_group group = GitHub::Ldap::PosixGroup.new(@ldap, @simple_group) - user = @ldap.domain("uid=benburkert,ou=users,dc=github,dc=com").bind + user = @ldap.domain("uid=user1,ou=People,dc=github,dc=com").bind assert group.is_member?(user), "Expected user in the memberUid list to be a member of the posixgroup" @@ -89,7 +86,7 @@ def test_is_member_simple_group def test_is_member_combined_group group = GitHub::Ldap::PosixGroup.new(@ldap, @one_level_deep_group) - user = @ldap.domain("uid=calavera,ou=users,dc=github,dc=com").bind + user = @ldap.domain("uid=user1,ou=People,dc=github,dc=com").bind assert group.is_member?(user), "Expected user in a subgroup to be a member of the posixgroup" @@ -97,7 +94,7 @@ def test_is_member_combined_group def test_is_not_member_simple_group group = GitHub::Ldap::PosixGroup.new(@ldap, @simple_group) - user = @ldap.domain("uid=calavera,ou=users,dc=github,dc=com").bind + user = @ldap.domain("uid=user10,ou=People,dc=github,dc=com").bind refute group.is_member?(user), "Expected user to not be member when her uid is not in the list of memberUid" @@ -105,7 +102,7 @@ def test_is_not_member_simple_group def test_is_member_combined_group group = GitHub::Ldap::PosixGroup.new(@ldap, @one_level_deep_group) - user = @ldap.domain("uid=benburkert,ou=users,dc=github,dc=com").bind + user = @ldap.domain("uid=user10,ou=People,dc=github,dc=com").bind refute group.is_member?(user), "Expected user to not be member when she's not member of any subgroup" diff --git a/test/referral_chaser_test.rb b/test/referral_chaser_test.rb new file mode 100644 index 0000000..3a19973 --- /dev/null +++ b/test/referral_chaser_test.rb @@ -0,0 +1,102 @@ +require_relative 'test_helper' + +class GitHubLdapReferralChaserTestCases < GitHub::Ldap::Test + + def setup + @ldap = GitHub::Ldap.new(options) + @chaser = GitHub::Ldap::ReferralChaser.new(@ldap) + end + + def test_creates_referral_with_connection_credentials + @ldap.expects(:search).yields({ search_referrals: ["ldap://dc1.ghe.local/"]}).returns([]) + + referral = mock("GitHub::Ldap::ReferralChaser::Referral") + referral.stubs(:search).returns([]) + + GitHub::Ldap::ReferralChaser::Referral.expects(:new) + .with("ldap://dc1.ghe.local/", "uid=admin,dc=github,dc=com", "passworD1", options[:port]) + .returns(referral) + + @chaser.search({}) + end + + def test_creates_referral_with_default_port + @ldap.expects(:search).yields({ + search_referrals: ["ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"] + }).returns([]) + + stub_referral_connection = mock("GitHub::Ldap") + stub_referral_connection.stubs(:search).returns([]) + GitHub::Ldap::ConnectionCache.expects(:get_connection).with(has_entry(port: options[:port])).returns(stub_referral_connection) + chaser = GitHub::Ldap::ReferralChaser.new(@ldap) + chaser.search({}) + end + + def test_creates_referral_for_first_referral_string + @ldap.expects(:search).multiple_yields([ + { search_referrals: + ["ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", + "ldap://dc2.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"] + } + ],[ + { search_referrals: + ["ldap://dc3.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", + "ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"] + } + ]).returns([]) + + referral = mock("GitHub::Ldap::ReferralChaser::Referral") + referral.stubs(:search).returns([]) + + GitHub::Ldap::ReferralChaser::Referral.expects(:new) + .with( + "ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", + "uid=admin,dc=github,dc=com", + "passworD1", + options[:port]) + .returns(referral) + + @chaser.search({}) + end + + def test_returns_referral_search_results + @ldap.expects(:search).multiple_yields([ + { search_referrals: + ["ldap://dc1.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", + "ldap://dc2.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"] + } + ],[ + { search_referrals: + ["ldap://dc3.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", + "ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local"] + } + ]).returns([]) + + referral = mock("GitHub::Ldap::ReferralChaser::Referral") + referral.expects(:search).returns(["result", "result"]) + + GitHub::Ldap::ReferralChaser::Referral.expects(:new).returns(referral) + + results = @chaser.search({}) + assert_equal(["result", "result"], results) + end + + def test_handle_blank_url_string_in_referral + @ldap.expects(:search).yields({ search_referrals: [""] }) + + results = @chaser.search({}) + assert_equal([], results) + end + + def test_returns_referral_search_results + @ldap.expects(:search).yields({ foo: ["not a referral"] }) + + GitHub::Ldap::ReferralChaser::Referral.expects(:new).never + results = @chaser.search({}) + end + + def test_referral_should_use_host_from_referral_string + GitHub::Ldap::ConnectionCache.expects(:get_connection).with(has_entry(host: "dc4.ghe.local")) + GitHub::Ldap::ReferralChaser::Referral.new("ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local", "", "") + end +end diff --git a/test/support/vm/activedirectory/.gitignore b/test/support/vm/activedirectory/.gitignore new file mode 100644 index 0000000..137e678 --- /dev/null +++ b/test/support/vm/activedirectory/.gitignore @@ -0,0 +1 @@ +env.sh diff --git a/test/support/vm/activedirectory/README.md b/test/support/vm/activedirectory/README.md new file mode 100644 index 0000000..36155bd --- /dev/null +++ b/test/support/vm/activedirectory/README.md @@ -0,0 +1,26 @@ +# Local ActiveDirectory Integration Testing + +Integration tests are not run for ActiveDirectory in continuous integration +because we cannot install a Windows VM on TravisCI. To test ActiveDirectory, +configure a local VM with AD running (this is left as an exercise for the +reader). + +To run integration tests against the local ActiveDirectory VM, from the project +root run: + +``` bash +# duplicate example env.sh for specific config +$ cp test/support/vm/activedirectory/env.sh{.example,} + +# edit env.sh and fill in with your VM's values, then +$ source test/support/vm/activedirectory/env.sh + +# run all tests against AD +$ time bundle exec rake + +# run a specific test file against AD +$ time bundle exec ruby test/membership_validators/active_directory_test.rb + +# reset environment to test other LDAP servers +$ source test/support/vm/activedirectory/reset-env.sh +``` diff --git a/test/support/vm/activedirectory/env.sh.example b/test/support/vm/activedirectory/env.sh.example new file mode 100644 index 0000000..3ca2c9b --- /dev/null +++ b/test/support/vm/activedirectory/env.sh.example @@ -0,0 +1,8 @@ +# Copy this to ad-env.sh, and fill in with your own values + +export TESTENV=activedirectory +export INTEGRATION_HOST=123.123.123.123 +export INTEGRATION_PORT=389 +export INTEGRATION_USER="CN=Administrator,CN=Users,DC=ad,DC=example,DC=com" +export INTEGRATION_PASSWORD='passworD1' +export INTEGRATION_SEARCH_DOMAINS='CN=Users,DC=example,DC=com' diff --git a/test/support/vm/activedirectory/reset-env.sh b/test/support/vm/activedirectory/reset-env.sh new file mode 100644 index 0000000..971423f --- /dev/null +++ b/test/support/vm/activedirectory/reset-env.sh @@ -0,0 +1,6 @@ +unset TESTENV +unset INTEGRATION_HOST +unset INTEGRATION_PORT +unset INTEGRATION_USER +unset INTEGRATION_PASSWORD +unset INTEGRATION_SEARCH_DOMAINS diff --git a/test/support/vm/openldap/.gitignore b/test/support/vm/openldap/.gitignore new file mode 100644 index 0000000..dace708 --- /dev/null +++ b/test/support/vm/openldap/.gitignore @@ -0,0 +1 @@ +/.vagrant diff --git a/test/support/vm/openldap/README.md b/test/support/vm/openldap/README.md new file mode 100644 index 0000000..ced5a63 --- /dev/null +++ b/test/support/vm/openldap/README.md @@ -0,0 +1,32 @@ +# Local OpenLDAP Integration Testing + +Set up a [Vagrant](http://www.vagrantup.com/) VM to run tests against OpenLDAP locally. + +To run tests against OpenLDAP (instead of ApacheDS) locally: + +``` bash +# start VM (from the correct directory) +$ cd test/support/vm/openldap/ +$ vagrant up + +# get the IP address of the VM +$ ip=$(vagrant ssh -- "ifconfig eth1 | grep -o -E '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1") + +# change back to root project directory +$ cd ../../../.. + +# run all tests against OpenLDAP +$ time TESTENV=openldap INTEGRATION_HOST=$ip bundle exec rake + +# run a specific test file against OpenLDAP +$ time TESTENV=openldap INTEGRATION_HOST=$ip bundle exec ruby test/membership_validators/recursive_test.rb + +# run OpenLDAP tests by default +$ export TESTENV=openldap +$ export TESTENV=$ip + +# now run tests without having to set ENV variables +$ time bundle exec rake +``` + +You may need to `gem install vagrant` first in order to provision the VM. diff --git a/test/support/vm/openldap/Vagrantfile b/test/support/vm/openldap/Vagrantfile new file mode 100644 index 0000000..abb44ae --- /dev/null +++ b/test/support/vm/openldap/Vagrantfile @@ -0,0 +1,35 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! +VAGRANTFILE_API_VERSION = "2" + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + config.vm.hostname = "openldap.github.org" + + config.vm.box = "hashicorp/precise64" + + config.vm.network "private_network", type: :dhcp + + config.ssh.forward_agent = true + + # config.vm.provision "shell", inline: "apt-get update; exec env /vagrant_data/script/install-openldap" + config.vm.provision "shell", inline: 'echo "HIIIIIII"', run: "always" + + config.vm.synced_folder "../../../..", "/vagrant_data" + + config.vm.provider "vmware_fusion" do |vb, override| + override.vm.box = "hashicorp/precise64" + vb.memory = 4596 + vb.vmx["displayname"] = "integration tests vm" + vb.vmx["numvcpus"] = "2" + end + + config.vm.provider "virtualbox" do |vb, override| + vb.memory = 4096 + vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"] + vb.customize ["modifyvm", :id, "--chipset", "ich9"] + vb.customize ["modifyvm", :id, "--vram", "16"] + end + +end diff --git a/test/test_helper.rb b/test/test_helper.rb index d996c5f..e92caa6 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -10,38 +10,92 @@ require 'github/ldap' require 'github/ldap/server' +require 'minitest/mock' require 'minitest/autorun' +require 'mocha/minitest' + +if ENV.fetch('TESTENV', "apacheds") == "apacheds" + # Make sure we clean up running test server + # NOTE: We need to do this manually since its internal `at_exit` hook + # collides with Minitest's autorun at_exit handling, hence this hook. + Minitest.after_run do + GitHub::Ldap.stop_server + end +end + class GitHub::Ldap::Test < Minitest::Test + def self.test_env + ENV.fetch("TESTENV", "apacheds") + end + def self.run(reporter, options = {}) start_server - super + result = super stop_server + result end def self.stop_server - GitHub::Ldap.stop_server + if test_env == "apacheds" + # see Minitest.after_run hook above. + # GitHub::Ldap.stop_server + end + end + + def self.test_server_options + { + custom_schemas: FIXTURES.join('posixGroup.schema.ldif').to_s, + user_fixtures: FIXTURES.join('common/seed.ldif').to_s, + allow_anonymous: true, + verbose: ENV.fetch("VERBOSE", "0") == "1" + } end def self.start_server - server_opts = respond_to?(:test_server_options) ? test_server_options : {} - GitHub::Ldap.start_server(server_opts) + if test_env == "apacheds" + # skip this if a server has already been started + return if GitHub::Ldap.ldap_server + + GitHub::Ldap.start_server(test_server_options) + end end def options @service = MockInstrumentationService.new - @options ||= GitHub::Ldap.server_options.merge \ - host: 'localhost', - uid: 'uid', - :instrumentation_service => @service + @options ||= + case self.class.test_env + when "apacheds" + GitHub::Ldap.server_options.merge \ + admin_user: 'uid=admin,dc=github,dc=com', + admin_password: 'passworD1', + host: 'localhost', + uid: 'uid', + instrumentation_service: @service + when "openldap" + { + host: ENV.fetch("INTEGRATION_HOST", "localhost"), + port: 389, + admin_user: 'uid=admin,dc=github,dc=com', + admin_password: 'passworD1', + search_domains: %w(dc=github,dc=com), + uid: 'uid', + instrumentation_service: @service + } + when "activedirectory" + { + host: ENV.fetch("INTEGRATION_HOST"), + port: ENV.fetch("INTEGRATION_PORT", 389), + admin_user: ENV.fetch("INTEGRATION_USER"), + admin_password: ENV.fetch("INTEGRATION_PASSWORD"), + search_domains: ENV.fetch("INTEGRATION_SEARCH_DOMAINS"), + instrumentation_service: @service + } + end end end class GitHub::Ldap::UnauthenticatedTest < GitHub::Ldap::Test - def self.start_server - GitHub::Ldap.start_server(:allow_anonymous => true) - end - def options @options ||= begin super.delete_if {|k, _| [:admin_user, :admin_password].include?(k)} diff --git a/test/url_test.rb b/test/url_test.rb new file mode 100644 index 0000000..db44ce2 --- /dev/null +++ b/test/url_test.rb @@ -0,0 +1,85 @@ +require_relative 'test_helper' + +class GitHubLdapURLTestCases < GitHub::Ldap::Test + + def setup + @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)") + end + + def test_host + assert_equal "dc4.ghe.local", @url.host + end + + def test_port + assert_equal 123, @url.port + end + + def test_scheme + assert_equal "ldap", @url.scheme + end + + def test_default_port + url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/CN=Maggie%20Mae,CN=Users,DC=dc4,DC=ghe,DC=local?attributes?scope?filter") + assert_equal 389, url.port + end + + def test_simple_url + url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local") + assert_equal 389, url.port + assert_equal "dc4.ghe.local", url.host + assert_equal "ldap", url.scheme + assert_equal "", url.dn + assert_equal nil, url.attributes + assert_equal nil, url.filter + assert_equal nil, url.scope + end + + def test_invalid_scheme + ex = assert_raises(GitHub::Ldap::URL::InvalidLdapURLException) do + GitHub::Ldap::URL.new("http://dc4.ghe.local") + end + assert_equal("Invalid LDAP URL: http://dc4.ghe.local", ex.message) + end + + def test_invalid_url + ex = assert_raises(GitHub::Ldap::URL::InvalidLdapURLException) do + GitHub::Ldap::URL.new("not a url") + end + assert_equal("Invalid LDAP URL: not a url", ex.message) + end + + def test_parse_dn + assert_equal "CN=Maggie Mae,CN=Users,DC=dc4,DC=ghe,DC=local", @url.dn + end + + def test_parse_attributes + assert_equal "cn,mail,telephoneNumber", @url.attributes + end + + def test_parse_filter + assert_equal "(cn=Charlie)", @url.filter + end + + def test_parse_scope + assert_equal "base", @url.scope + end + + def test_default_scope + url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/base_dn?cn=joe??filter") + assert_equal "", url.scope + end + + def test_net_ldap_scopes + sub_scope_url = GitHub::Ldap::URL.new("ldap://ghe.local/base_dn?cn=joe?sub?filter") + one_scope_url = GitHub::Ldap::URL.new("ldap://ghe.local/base_dn?cn=joe?one?filter") + base_scope_url = GitHub::Ldap::URL.new("ldap://ghe.local/base_dn?cn=joe?base?filter") + default_scope_url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/base_dn?cn=joe??filter") + invalid_scope_url = GitHub::Ldap::URL.new("ldap://dc4.ghe.local/base_dn?cn=joe?invalid?filter") + + assert_equal Net::LDAP::SearchScope_BaseObject, base_scope_url.net_ldap_scope + assert_equal Net::LDAP::SearchScope_SingleLevel, one_scope_url.net_ldap_scope + assert_equal Net::LDAP::SearchScope_WholeSubtree, sub_scope_url.net_ldap_scope + assert_equal Net::LDAP::SearchScope_BaseObject, default_scope_url.net_ldap_scope + assert_equal Net::LDAP::SearchScope_BaseObject, invalid_scope_url.net_ldap_scope + end +end diff --git a/test/user_search/active_directory_test.rb b/test/user_search/active_directory_test.rb new file mode 100644 index 0000000..32bed79 --- /dev/null +++ b/test/user_search/active_directory_test.rb @@ -0,0 +1,53 @@ +require_relative '../test_helper' + +class GitHubLdapActiveDirectoryUserSearchTests < GitHub::Ldap::Test + + def test_global_catalog_returns_empty_array_for_no_results + ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev')) + ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(ldap) + + mock_global_catalog_connection = mock("GitHub::Ldap::UserSearch::GlobalCatalog") + mock_global_catalog_connection.expects(:search).returns(nil) + ad_user_search.expects(:global_catalog_connection).returns(mock_global_catalog_connection) + results = ad_user_search.perform("login", "CN=Joe", "uid", {}) + assert_equal [], results + end + + def test_global_catalog_returns_array_of_results + ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev')) + ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(ldap) + + mock_global_catalog_connection = mock("GitHub::Ldap::UserSearch::GlobalCatalog") + stub_entry = mock("Net::LDAP::Entry") + + mock_global_catalog_connection.expects(:search).returns([stub_entry]) + ad_user_search.expects(:global_catalog_connection).returns(mock_global_catalog_connection) + + results = ad_user_search.perform("login", "CN=Joe", "uid", {}) + assert_equal [stub_entry], results + end + + def test_searches_with_empty_base_dn + ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev')) + ad_user_search = GitHub::Ldap::UserSearch::ActiveDirectory.new(ldap) + + mock_global_catalog_connection = mock("GitHub::Ldap::UserSearch::GlobalCatalog") + mock_global_catalog_connection.expects(:search).with(has_entry(:base => "")) + ad_user_search.expects(:global_catalog_connection).returns(mock_global_catalog_connection) + ad_user_search.perform("login", "CN=Joe", "uid", {}) + end + + def test_global_catalog_default_settings + ldap = GitHub::Ldap.new(options.merge(host: 'ghe.dev')) + global_catalog = GitHub::Ldap::UserSearch::GlobalCatalog.connection(ldap) + instrumentation_service = global_catalog.instance_variable_get(:@instrumentation_service) + + auth = global_catalog.instance_variable_get(:@auth) + assert_equal :simple, auth[:method] + assert_equal "uid=admin,dc=github,dc=com", auth[:username] + assert_equal "passworD1", auth[:password] + assert_equal "ghe.dev", global_catalog.host + assert_equal 3268, global_catalog.port + assert_equal "MockInstrumentationService", instrumentation_service.class.name + end +end diff --git a/test/user_search/default_test.rb b/test/user_search/default_test.rb new file mode 100644 index 0000000..abc230a --- /dev/null +++ b/test/user_search/default_test.rb @@ -0,0 +1,19 @@ +require_relative '../test_helper' + +class GitHubLdapActiveDirectoryUserSearchTests < GitHub::Ldap::Test + def setup + @ldap = GitHub::Ldap.new(options) + @default_user_search = GitHub::Ldap::UserSearch::Default.new(@ldap) + end + + def test_default_search_options + @ldap.expects(:search).with(has_entries( + attributes: [], + size: 1, + paged_searches_supported: true, + base: "CN=HI,CN=McDunnough", + filter: kind_of(Net::LDAP::Filter) + )) + @default_user_search.perform("","CN=HI,CN=McDunnough","",{}) + end +end