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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy