diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4c1101d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +max_line_length = 120 +trim_trailing_whitespace = true + +[*.rdoc] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 4bef38f..529bb5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,20 @@ -*.swp -.idea -.bin -.bundle -*.gem +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ + +/Gemfile.lock .rvmrc .ruby-gemset .ruby-version -Gemfile.lock +*.gem +/vendor/bundle +# Misc. +.DS_Store +*.swp +/.idea/ diff --git a/.rubocop.yml b/.rubocop.yml index 397c814..eac33e9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,92 +1,36 @@ -LineLength: - Max: 120 - -ParameterLists: - Max: 5 - -MethodLength: - Max: 15 - -CollectionMethods: - # remove this once we pick either map or collect, inject or reduce, etc - PreferredMethods: {} +AllCops: + TargetRubyVersion: 2.3 -StringLiterals: +Style/Documentation: Enabled: false -SpaceAroundBraces: - Enabled: false # remove me - -RedundantSelf: - Enabled: false # remove me - -Encoding: - Enabled: false # remove me? - -RedundantReturn: - Enabled: false # remove me - -SpaceInsideHashLiteralBraces: - Enabled: false # remove me - -Void: - Enabled: false # remove me - -BlockNesting: - Enabled: false # remove me - -AvoidPerlBackrefs: - Enabled: false # remove me - -Documentation: - Enabled: false # remove me - -# end TODO - -# AllCops: -# Excludes: - -SpaceInsideHashLiteralBraces: - EnforcedStyleIsWithSpaces: false - -HashSyntax: - Enabled: false # don't force 1.9 hash syntax - -SpaceInsideHashLiteralBraces: - Enabled: false # allow spaces (eg { :a => 1 }) - -LeadingCommentSpace: - Enabled: false - -IfUnlessModifier: - Enabled: false +Metrics/LineLength: + Max: 120 -RescueModifier: +Gemspec/RequiredRubyVersion: Enabled: false -AssignmentInCondition: +Style/ExpandPathArguments: Enabled: false -FavorUnlessOverNegatedIf: - Enabled: false +Style/PercentLiteralDelimiters: + PreferredDelimiters: + '%w': '[]' -WhileUntilModifier: +Style/SymbolArray: Enabled: false -AlignParameters: - Enabled: false # don't care if parameters are not aligned - -ParenthesesAroundCondition: - Enabled: false +Naming/UncommunicativeMethodParamName: + AllowedNames: + - cn + - dn -DotPosition: +Style/ClassAndModuleChildren: Enabled: false -Lambda: - Enabled: false # don't require -> for single line lambdas - -HashMethods: - Enabled: false +Metrics/ClassLength: + Exclude: + - 'test/**/*' -ReduceArguments: +Layout/EndOfLine: Enabled: false diff --git a/.travis.yml b/.travis.yml index e20b2b6..1e91279 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,11 @@ sudo: false language: ruby +cache: bundler rvm: - - '1.9.3' - - '2.0.0' - - '2.0.0' - - '2.1.8' - - '2.2.4' - - '2.3.0' - -script: - - "./travis/ldapfluff_up_to_snuff.sh" + - 1.9.3 + - 2.0 + - 2.3 + - ruby + - jruby +before_script: bundle exec rubocop +script: bundle exec rake diff --git a/Gemfile b/Gemfile index 1293be4..0cace49 100644 --- a/Gemfile +++ b/Gemfile @@ -1,15 +1,6 @@ -# vim:ft=ruby +# frozen_string_literal: true source 'https://rubygems.org' -if RUBY_VERSION.start_with? '1.9' - gem 'net-ldap', '< 0.13' -end - -unless RUBY_VERSION >= '2.2' - gem 'activesupport', '< 5' -end - +# Specify gem's dependencies in the gemspec file gemspec - -gem 'rubocop', :group => :test if RUBY_VERSION >= '2.0' diff --git a/README.rdoc b/README.rdoc index 6f93f8d..89ba327 100644 --- a/README.rdoc +++ b/README.rdoc @@ -25,7 +25,7 @@ It exposes these methods: user_list(gid) returns the set of users that belong to an LDAP group - is_in_groups?(uid, grouplist) + user_in_groups?(uid, grouplist) returns true if the user provided is in all of the groups listed in grouplist valid_user?(uid) @@ -34,10 +34,10 @@ It exposes these methods: valid_group?(uid) returns true if the group provided exists - find_user(uid) + find_user(uid, only = nil) returns the LDAP entry of the user if found, nil if not found - find_group(gid) + find_group(gid, only = nil) returns the LDAP entry of the group if found, nil if not found These methods are handy for using LDAP for both authentication and authorization. @@ -49,12 +49,12 @@ Your global configuration must provide information about your LDAP host to funct host: # ip address or hostname port: # port encryption: # blank, :simple_tls, or :start_tls - base_dn: # base DN for LDAP auth, eg dc=redhat,dc=com + base_dn: # base DN for LDAP auth, eg dc=redhat,dc=com group_base: # base DN for your LDAP groups, eg ou=Groups,dc=redhat,dc=com use_netgroups: # false by default, use true if you want to use netgroup triples, # supported only for server type :free_ipa and :posix - server_type: # type of server. default == :posix. :active_directory, :posix, :free_ipa - ad_domain: # domain for your users if using active directory, eg redhat.com + server_type: # type of server. default == :posix. :active_directory, :posix, :free_ipa + ad_domain: # domain for your users if using active directory, eg redhat.com service_user: # service account for authenticating LDAP calls. required unless you enable anon service_pass: # service password for authenticating LDAP calls. required unless you enable anon anon_queries: # false by default, true if you don't want to use the service user @@ -62,10 +62,12 @@ Your global configuration must provide information about your LDAP host to funct You can pass these arguments as a hash to LdapFluff to get a valid LdapFluff object. - ldap_config = { :host => "freeipa.localdomain", :port => 389, :encryption => nil, :base_dn => "DC=mydomain,DC=com", - :group_base => "DC=groups,DC=mydomain,DC=com", :attr_login => "uid", :server_type => :free_ipa, - :service_user => "admin", :search_filter => "(objectClass=*)", :service_pass => "mypass", - :anon_queries => false } + ldap_config = { + :host => "freeipa.localdomain", :port => 389, :encryption => nil, :base_dn => "DC=mydomain,DC=com", + :group_base => "DC=groups,DC=mydomain,DC=com", :attr_login => "uid", :server_type => :free_ipa, + :service_user => "admin", :search_filter => "(objectClass=*)", :service_pass => "mypass", + :anon_queries => false + } fluff = LdapFluff.new(ldap_config) fluff.valid_user?("admin") # returns true @@ -99,6 +101,13 @@ ActiveSupport::Notifications. ldap_fluff will use this and also pass it to net- When using Rails, pass `:instrumentation_service => ActiveSupport::Notifications` and then subscribe to, and optionally log events (e.g. https://gist.github.com/mnutt/566725). -=== License +== Development + +After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rake` to run the tests. +You can also run `irb -r bundler/setup -r ldap_fluff` for an interactive prompt that will allow you to experiment. + +To install this gem onto your local machine, run `bundle exec rake install`. + +== License ldap_fluff is licensed under the GPLv2. Please read LICENSE for more information. diff --git a/Rakefile b/Rakefile index 3c68ce9..4caa98e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,14 +1,12 @@ -require 'rubygems' -require 'rake/testtask' - -# The default task is run if rake is given no explicit arguments. -desc 'Default Task' -task :default => :test +# frozen_string_literal: true -# Test Tasks --------------------------------------------------------- +require 'bundler/gem_tasks' +require 'rake/testtask' -Rake::TestTask.new('test') do |t| - t.libs = %w[lib test] - t.test_files = FileList['test/**/*.rb'] - t.verbose = true +Rake::TestTask.new(:test) do |t| + t.libs << 'test' + t.libs << 'lib' + t.test_files = FileList['test/**/*_test.rb'] end + +task default: :test diff --git a/ldap_fluff.gemspec b/ldap_fluff.gemspec index 5d7bf93..6bf5610 100644 --- a/ldap_fluff.gemspec +++ b/ldap_fluff.gemspec @@ -1,24 +1,30 @@ +# frozen_string_literal: true + Gem::Specification.new do |s| s.name = 'ldap_fluff' - s.version = '0.4.7' + s.version = '0.5.1' s.summary = 'LDAP querying tools for Active Directory, FreeIPA and POSIX-style' s.description = 'Simple library for binding & group querying on top of various LDAP implementations' s.homepage = 'https://github.com/theforeman/ldap_fluff' s.license = 'GPLv2' - s.files = Dir['lib/**/*.rb'] + Dir['test/**/*.rb'] + ['README.rdoc', 'LICENSE'] - s.extra_rdoc_files = ['README.rdoc', 'LICENSE'] - s.require_path = 'lib' - s.test_files = Dir['test/**/*.rb'] + s.extra_rdoc_files = %w[README.rdoc LICENSE] + s.files = s.extra_rdoc_files + Dir['lib/**/*.rb'] + + s.require_paths = ['lib'] + s.test_files = Dir['test/**/*.rb'] + + s.authors = ['Jordan O\'Mara', 'Daniel Lobato', 'Petr Chalupa', 'Adam Price', 'Marek Hulan', 'Dominic Cleal'] + s.email = %w[jomara@redhat.com elobatocs@gmail.com pchalupa@redhat.com komidore64@gmail.com mhulan@redhat.com + dominic@cleal.org] + + s.required_ruby_version = '>= 1.9.3' - s.has_rdoc = true - s.author = ['Jordan O\'Mara', 'Daniel Lobato', 'Petr Chalupa', 'Adam Price', 'Marek Hulan', 'Dominic Cleal'] - s.email = %w(jomara@redhat.com elobatocs@gmail.com pchalupa@redhat.com komidore64@gmail.com mhulan@redhat.com dominic@cleal.org) + s.add_dependency 'net-ldap', '~> 0.12' - s.required_ruby_version = ">= 1.9.3" + s.add_development_dependency 'bundler', '>= 1.14' + s.add_development_dependency 'rake', '>= 10.0' - s.add_dependency('net-ldap', '>= 0.3.1') - s.add_dependency('activesupport') - s.add_development_dependency('rake') - s.add_development_dependency('minitest') + s.add_development_dependency 'minitest', '~> 5.0' + s.add_development_dependency 'rubocop' end diff --git a/lib/ldap_fluff.rb b/lib/ldap_fluff.rb index 8c68853..d2d9103 100644 --- a/lib/ldap_fluff.rb +++ b/lib/ldap_fluff.rb @@ -1,6 +1,9 @@ +# frozen_string_literal: true + +require 'net/ldap' + require 'ldap_fluff/error' require 'ldap_fluff/config' -require 'ldap_fluff/ldap_fluff' require 'ldap_fluff/generic' require 'ldap_fluff/generic_member_service' require 'ldap_fluff/active_directory' @@ -11,3 +14,122 @@ require 'ldap_fluff/freeipa' require 'ldap_fluff/freeipa_member_service' require 'ldap_fluff/freeipa_netgroup_member_service' + +class LdapFluff + # @!attribute [rw] ldap + # @return [Generic] + # @!attribute [rw] instrumentation_service + # @return [#instrument] + attr_accessor :ldap, :instrumentation_service + + def initialize(config = {}) + config = Config.new(config) + + @ldap = create_provider(config) + @instrumentation_service = config.instrumentation_service + end + + # @param [String] uid + # @param [String] password + # @return [Boolean] + def authenticate?(uid, password) + instrument('authenticate.ldap_fluff', uid: uid) do |payload| + !password || password.empty? ? false : ldap.bind?(payload[:uid], password) + end + end + + def test + instrument('test.ldap_fluff') do # |payload| + ldap.ldap.open {} + end + end + + # @param [String] gid + # @return [Array] a list of users for a given gid + def user_list(gid) + instrument('user_list.ldap_fluff', gid: gid) do |payload| + ldap.users_for_gid payload[:gid] + end + end + + # @param [String] uid + # @return [Array] a list of groups for a given uid + def group_list(uid) + instrument('group_list.ldap_fluff', uid: uid) do |payload| + ldap.groups_for_uid payload[:uid] + end + end + + # @param [String] uid + # @return [Boolean] true if a user is in all of the groups in grouplist + def user_in_groups?(uid, grouplist) + instrument('user_in_groups?.ldap_fluff', uid: uid, grouplist: grouplist) do |payload| + ldap.user_in_groups? payload[:uid], payload[:grouplist], true + end + end + + # @param [String] uid + # @return [Boolean] true if uid exists + def valid_user?(uid) + instrument('valid_user?.ldap_fluff', uid: uid) do |payload| + ldap.user_exists? payload[:uid] + end + end + + # @param [String] gid + # @return [Boolean] true if group exists + def valid_group?(gid) + instrument('valid_group?.ldap_fluff', gid: gid) do |payload| + ldap.group_exists? payload[:gid] + end + end + + # @param [String] uid + # @return [Array, Net::LDAP::Entry] + def find_user(uid, only = nil) + instrument('find_user.ldap_fluff', uid: uid) do |payload| + ldap.member_service.find_user payload[:uid], only + end + end + + # @param [String] gid + # @return [Array, Net::LDAP::Entry] + def find_group(gid, only = nil) + instrument('find_group.ldap_fluff', gid: gid) do |payload| + ldap.member_service.find_group payload[:gid], only + end + end + + private + + # @param [Config] config + # @return [Generic] + # @raise [RuntimeError] + def create_provider(config) + case config.server_type + when :posix + Posix.new(config) + when :active_directory + ActiveDirectory.new(config) + when :free_ipa + FreeIPA.new(config) + else + raise 'unknown server_type' + end + end + + # @param [String] event + # @param [Hash] payload + # @yieldreturn [Hash] + def instrument(event, payload = {}) + payload = payload ? payload.dup : {} + + if instrumentation_service + instrumentation_service.instrument(event, payload) do |data| + data[:result] = yield(data) if block_given? + end + elsif block_given? + yield(payload) + end + end +end diff --git a/lib/ldap_fluff/active_directory.rb b/lib/ldap_fluff/active_directory.rb index 999b8f3..1d894b6 100644 --- a/lib/ldap_fluff/active_directory.rb +++ b/lib/ldap_fluff/active_directory.rb @@ -1,50 +1,56 @@ -class LdapFluff::ActiveDirectory < LdapFluff::Generic +# frozen_string_literal: true - def bind?(uid = nil, password = nil, opts = {}) - unless uid.include?(',') || uid.include?('\\') || opts[:search] == false - service_bind - user = @member_service.find_user(uid) - uid = user.first.dn if user && user.first - end - @ldap.auth(uid, password) - @ldap.bind +class LdapFluff::ActiveDirectory < LdapFluff::Generic + # @param [LdapFluff::Config] config + def initialize(config) + config.bind_dn_format ||= "%s@#{config.base_dn.scan(/\bDC=([^,]*)/i).flatten.join('.')}" + super + @is_bind_dn = /(? 0) - rescue MemberService::UIDNotFoundException - return false - end + # + # @param [String] uid + # @param [Array] gids + # @return [Boolean] + def user_in_groups?(uid, gids = [], all = false) + super + rescue MemberService::UIDNotFoundException + false end private + # @param [Net::LDAP::Entry] search + # @param [Symbol] method + # @return [Array] def users_from_search_results(search, method) - users = [] + members = search.send method - search.send(method).each do |member| + # @type [Array] + users = members.map do |member| begin - entry = @member_service.find_by_dn(member).first + entry = member_service.find_by_dn(member, true) rescue MemberService::UIDNotFoundException - next + entry = nil end - objectclasses = entry.objectclass.map(&:downcase) - if (%w(organizationalperson person userproxy) & objectclasses).present? - users << @member_service.get_login_from_entry(entry) - elsif (%w(organizationalunit group) & objectclasses).present? - users << users_for_gid(entry.cn.first) - end + entry ? get_users_for_entry(entry) : nil end - users.flatten.uniq + users.flatten.compact.uniq end + # @param [Net::LDAP::Entry] entry + # @return [Array, String] + def get_users_for_entry(entry) + objectclasses = entry[:objectclass].map(&:downcase) + + if !(%w[organizationalperson person userproxy] & objectclasses).empty? + member_service.get_login_from_entry(entry) + elsif !(%w[organizationalunit group] & objectclasses).empty? + users_for_gid(entry[:cn].first) + end + end end diff --git a/lib/ldap_fluff/ad_member_service.rb b/lib/ldap_fluff/ad_member_service.rb index ae1f42c..6b2b306 100644 --- a/lib/ldap_fluff/ad_member_service.rb +++ b/lib/ldap_fluff/ad_member_service.rb @@ -1,50 +1,63 @@ -require 'net/ldap' +# frozen_string_literal: true -# Naughty bits of active directory ldap queries +# Naughty bits of active directory LDAP queries class LdapFluff::ActiveDirectory::MemberService < LdapFluff::GenericMemberService - + # @param [Net::LDAP] ldap + # @param [LdapFluff::Config] config def initialize(ldap, config) - @attr_login = (config.attr_login || 'samaccountname') + config.attr_login ||= 'samaccountname' super end - # get a list [] of ldap groups for a given user - # in active directory, this means a recursive lookup + # get a list of LDAP groups for a given user in active directory, this means a recursive lookup + # @param [String] uid + # @return [Array] def find_user_groups(uid) - data = find_user(uid) - _groups_from_ldap_data(data.first) + # @type [Net::LDAP::Entry] + data = find_user(uid, true) + groups_from_ldap_data(data) end - # return the :memberof attrs + parents, recursively - def _groups_from_ldap_data(payload) - data = [] - if !payload.nil? - first_level = payload[:memberof] - total_groups, _ = _walk_group_ancestry(first_level, first_level) - data = (get_groups(first_level + total_groups)).uniq - end - data + private + + # @param [Net::LDAP::Entry] payload + # @return [Array] the :memberof attrs + parents, recursively + def groups_from_ldap_data(payload) + return [] unless payload + + first_level = payload[:memberof] + total_groups, = walk_group_ancestry(first_level, first_level) + + get_groups(first_level + total_groups).uniq end # recursively loop over the parent list - def _walk_group_ancestry(group_dns = [], known_groups = []) - set = [] + # @param [Array] group_dns + # @param [Array] known_groups + # @return [Array>] + def walk_group_ancestry(group_dns = [], known_groups = [], set = []) group_dns.each do |group_dn| - search = @ldap.search(:base => group_dn, :scope => Net::LDAP::SearchScope_BaseObject, :attributes => ['memberof']) - if !search.nil? && !search.first.nil? - groups = search.first[:memberof] - known_groups - known_groups += groups - next_level, new_known_groups = _walk_group_ancestry(groups, known_groups) - set += next_level - set += groups - known_groups += next_level - end + groups = find_parent_groups(group_dn) + next unless groups + + groups -= known_groups + known_groups += groups + next_level, = walk_group_ancestry(groups, known_groups) # new_known_groups + + set += next_level + groups + known_groups += next_level end + [set, known_groups] end - def class_filter - Net::LDAP::Filter.eq("objectclass", "group") + # @param [String] group_dn + # @return [Array] + def find_parent_groups(group_dn) + search = ldap.search(base: group_dn, scope: Net::LDAP::SearchScope_BaseObject, attributes: ['memberof']) + + search = search.first if search + search ? search[:memberof] : nil end class UIDNotFoundException < LdapFluff::Error diff --git a/lib/ldap_fluff/config.rb b/lib/ldap_fluff/config.rb index 92517a6..44cb09a 100644 --- a/lib/ldap_fluff/config.rb +++ b/lib/ldap_fluff/config.rb @@ -1,25 +1,66 @@ -require 'yaml' -require 'active_support/core_ext/hash' +# frozen_string_literal: true class LdapFluff::Config - ATTRIBUTES = %w[host port encryption base_dn group_base server_type service_user - service_pass anon_queries attr_login search_filter - instrumentation_service use_netgroups] - ATTRIBUTES.each { |attr| attr_reader attr.to_sym } - - DEFAULT_CONFIG = { 'port' => 389, - 'encryption' => nil, - 'base_dn' => 'dc=company,dc=com', - 'group_base' => 'dc=company,dc=com', - 'server_type' => :free_ipa, - 'anon_queries' => false, - 'instrumentation_service' => nil, - 'use_netgroups' => false } + ATTRIBUTES = [ + :host, :port, :encryption, :base_dn, :group_base, :server_type, :service_user, :service_pass, + :anon_queries, :attr_login, :search_filter, :instrumentation_service, :use_netgroups, + :bind_dn_format, :attr_member + ].freeze + + DEFAULT_CONFIG = { + port: 389, + encryption: nil, + base_dn: 'dc=company,dc=com', + group_base: 'dc=company,dc=com', + server_type: :free_ipa, + anon_queries: false, + attr_login: nil, + search_filter: nil, + instrumentation_service: nil, + use_netgroups: false, + bind_dn_format: nil, + attr_member: nil + }.freeze + + # @!attribute [rw] host + # @return [String] + # @!attribute [rw] port + # @return [Integer] + # @!attribute [rw] encryption + # @return [Symbol, Hash] + # @!attribute [rw] base_dn + # @return [String] + # @!attribute [rw] group_base + # @return [String] + # @!attribute [rw] server_type + # @return [Symbol] + # @!attribute [rw] service_user + # @return [String] + # @!attribute [rw] service_pass + # @return [String] + # @!attribute [rw] anon_queries + # @return [Boolean] + # @!attribute [rw] attr_login + # @return [String] + # @!attribute [rw] search_filter + # @return [String] + # @!attribute [rw] instrumentation_service + # @return [#instrument] + # @!attribute [rw] use_netgroups + # @return [Boolean] + # @!attribute [rw] bind_dn_format + # @return [String] + # @!attribute [rw] attr_member + # @return [String] + attr_accessor(*ATTRIBUTES) + # @param [#to_hash] config + # @raise [ArgumentError] if config is not a Hash + # @raise [ConfigError] if config contains invalid keys def initialize(config) raise ArgumentError unless config.respond_to?(:to_hash) - config = validate(convert(config)) + config = validate(convert(config)) ATTRIBUTES.each do |attr| instance_variable_set(:"@#{attr}", config[attr]) end @@ -28,57 +69,71 @@ def initialize(config) private # @param [#to_hash] config + # @return [Hash] def convert(config) - config.to_hash.with_indifferent_access.tap do |conf| - %w[encryption server_type method].each do |key| - conf[key] = conf[key].is_a?(Hash) ? convert(conf[key]) : conf[key].to_sym if conf[key] + Hash[ + config.to_hash.map do |key, val| + key = key.to_sym if key.respond_to?(:to_sym) + + if val && [:encryption, :server_type, :method].include?(key) + val = val.is_a?(Hash) ? convert(val) : val.to_sym + end + + [key, val] end - end + ] end - def missing_keys?(config) + # @param [Hash] config + def check_missing_keys(config) missing_keys = ATTRIBUTES - config.keys raise ConfigError, "missing configuration for keys: #{missing_keys.join(',')}" unless missing_keys.empty? end - def unknown_keys?(config) + # @param [Hash] config + def check_unknown_keys(config) unknown_keys = config.keys - ATTRIBUTES raise ConfigError, "unknown configuration keys: #{unknown_keys.join(',')}" unless unknown_keys.empty? end - def all_required_keys?(config) - %w[host port base_dn group_base server_type].all? do |key| - raise ConfigError, "config key #{key} has to be set, it was nil" if config[key].nil? + # @param [Hash] config + def check_required_keys(config) + [:host, :port, :base_dn, :group_base, :server_type].each do |key| + raise ConfigError, "config key #{key} has to be set, it was nil" unless config[key] end - %w[service_user service_pass].all? do |key| - if !config['anon_queries'] && config[key].nil? - raise ConfigError, "config key #{key} has to be set, it was nil" - end + [:service_user, :service_pass].each do |key| + raise ConfigError, "config key #{key} has to be set, it was nil" unless config[:anon_queries] || config[key] end end - def anon_queries_set?(config) - unless [false, true].include?(config['anon_queries']) - raise ConfigError, "config key anon_queries has to be true or false but was #{config['anon_queries']}" - end + # @param [Hash] config + def check_anon_queries_set(config) + return if [false, true].include?(config[:anon_queries]) + + raise ConfigError, "config key anon_queries has to be true or false but was #{config[:anon_queries]}" end - def correct_server_type?(config) - unless [:posix, :active_directory, :free_ipa].include?(config['server_type']) - raise ConfigError, 'config key server_type has to be :active_directory, :posix, :free_ipa ' + - "but was #{config['server_type']}" - end + # @param [Hash] config + def check_server_type(config) + return if [:posix, :active_directory, :free_ipa].include?(config[:server_type]) + + raise ConfigError, + "config key server_type has to be :active_directory, :posix, :free_ipa but was #{config[:server_type]}" end + # @param [Hash] config + # @return [Hash] + # @raise [ConfigError] if config contains invalid keys def validate(config) config = DEFAULT_CONFIG.merge(config) + config[:group_base] = config[:base_dn] if !config[:group_base] || config[:group_base].empty? - correct_server_type?(config) - missing_keys?(config) - unknown_keys?(config) - all_required_keys?(config) - anon_queries_set?(config) + check_server_type(config) + check_missing_keys(config) + check_unknown_keys(config) + check_required_keys(config) + check_anon_queries_set(config) config end diff --git a/lib/ldap_fluff/error.rb b/lib/ldap_fluff/error.rb index 27c054c..c983c5d 100644 --- a/lib/ldap_fluff/error.rb +++ b/lib/ldap_fluff/error.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + class LdapFluff class Error < StandardError end end - diff --git a/lib/ldap_fluff/freeipa.rb b/lib/ldap_fluff/freeipa.rb index 05cf204..17ca850 100644 --- a/lib/ldap_fluff/freeipa.rb +++ b/lib/ldap_fluff/freeipa.rb @@ -1,46 +1,48 @@ -class LdapFluff::FreeIPA < LdapFluff::Generic +# frozen_string_literal: true - def bind?(uid = nil, password = nil, opts = {}) - unless uid.include?(',') - unless opts[:search] == false - service_bind - user = @member_service.find_user(uid) - end - uid = user && user.first ? user.first.dn : "uid=#{uid},cn=users,cn=accounts,#{@base}" - end - @ldap.auth(uid, password) - @ldap.bind +class LdapFluff::FreeIPA < LdapFluff::Generic + # @param [LdapFluff::Config] config + def initialize(config) + config.bind_dn_format ||= "uid=%s,cn=users,cn=accounts,#{config.base_dn}" + super end + # @param [String] uid + # @return [Array] + # @raise [UnauthenticatedException] def groups_for_uid(uid) - begin - super - rescue MemberService::InsufficientQueryPrivilegesException - raise UnauthenticatedException, "Insufficient Privileges to query groups data" - end + super + rescue MemberService::InsufficientQueryPrivilegesException + raise UnauthenticatedException, 'Insufficient Privileges to query groups data' end private + # Member results come in the form uid=sampleuser,cn=users, etc.. or gid=samplegroup,cn=groups + # @param [Net::LDAP::Entry] search + # @param [Symbol] method + # @return [Array] def users_from_search_results(search, method) - # Member results come in the form uid=sampleuser,cn=users, etc.. or gid=samplegroup,cn=groups - users = [] + members = search.send method - members = search.send(method) - - if method == :nisnetgrouptriple - users = @member_service.get_netgroup_users(members) - else - members.each do |member| - type = member.downcase.split(',')[1] - if type == 'cn=users' - users << @member_service.get_logins([member]) - elsif type == 'cn=groups' - users << users_for_gid(member.split(',')[0].split('=')[1]) - end + # @type [Array] + users = + if method == :nisnetgrouptriple + member_service.get_netgroup_users(members) + else + members.map { |member| get_users_for_member(member) } end - end - users.flatten.uniq + users.flatten.compact.uniq + end + + # @param [String] member DN + # @return [Array, String] + def get_users_for_member(member) + if member =~ /,(cn|ou)=users(,|$)/i + member_service.get_logins([member]) + elsif member =~ /,(cn|ou)=groups(,|$)/i + users_for_gid(member.sub(/^.*?=([^,]*).*/, '\1')) + end end end diff --git a/lib/ldap_fluff/freeipa_member_service.rb b/lib/ldap_fluff/freeipa_member_service.rb index 4dd2a6a..4e8e3b5 100644 --- a/lib/ldap_fluff/freeipa_member_service.rb +++ b/lib/ldap_fluff/freeipa_member_service.rb @@ -1,32 +1,36 @@ -require 'net/ldap' +# frozen_string_literal: true class LdapFluff::FreeIPA::MemberService < LdapFluff::GenericMemberService - + # @param [Net::LDAP] ldap + # @param [LdapFluff::Config] config def initialize(ldap, config) - @attr_login = (config.attr_login || 'uid') + config.attr_login ||= 'uid' super end - # return an ldap user with groups attached - # note : this method is not particularly fast for large ldap systems + # @param [String] uid + # @return [Array] an LDAP user with groups attached + # @note this method is not particularly fast for large LDAP systems def find_user_groups(uid) user = find_user(uid) - # if group data is missing, they aren't querying with a user - # with enough privileges - user.delete_if { |u| u.nil? || !u.respond_to?(:attribute_names) || !u.attribute_names.include?(:memberof) } - raise InsufficientQueryPrivilegesException if user.size < 1 - get_groups(user[0][:memberof]) + + # if group data is missing, they aren't querying with a user with enough privileges + user.delete_if { |u| !u.respond_to?(:attribute_names) || !u.attribute_names.include?(:memberof) } + raise InsufficientQueryPrivilegesException if user.empty? + + get_groups(user.first[:memberof]) end # extract the group names from the LDAP style response, - # return string will be something like - # CN=bros,OU=bropeeps,DC=jomara,DC=redhat,DC=com + # @param [Array] grouplist + # @return [Array] will be something like CN=bros,OU=bropeeps,DC=jomara,DC=redhat,DC=com def get_groups(grouplist) - grouplist.map(&:downcase).collect do |g| - if g.match(/.*?ipauniqueid=(.*?)/) - @ldap.search(:base => g)[0][:cn][0] + grouplist.map do |g| + if g =~ /.*?\bipaUniqueID=/i + search = (ldap.search(base: g) || []).first + search ? search[:cn].first : nil else - g.sub(/.*?cn=(.*?),.*/, '\1') + g.sub(/.*?\bCN=([^,]*).*/i, '\1') end end.compact end @@ -39,6 +43,4 @@ class GIDNotFoundException < LdapFluff::Error class InsufficientQueryPrivilegesException < LdapFluff::Error end - end - diff --git a/lib/ldap_fluff/freeipa_netgroup_member_service.rb b/lib/ldap_fluff/freeipa_netgroup_member_service.rb index 7438ba0..eec2790 100644 --- a/lib/ldap_fluff/freeipa_netgroup_member_service.rb +++ b/lib/ldap_fluff/freeipa_netgroup_member_service.rb @@ -1,14 +1,16 @@ -require 'net/ldap' +# frozen_string_literal: true class LdapFluff::FreeIPA::NetgroupMemberService < LdapFluff::FreeIPA::MemberService - + # @param [String] uid + # @return [Array] def find_user_groups(uid) - groups = [] - @ldap.search(:filter => Net::LDAP::Filter.eq('objectClass', 'nisNetgroup'), :base => @group_base).each do |entry| + groups = ldap.search(filter: class_filter('nisNetgroup'), base: config.group_base) + return [] unless groups + + groups.map do |entry| members = get_netgroup_users(entry[:nisnetgrouptriple]) - groups << entry[:cn][0] if members.include? uid - end - groups + + members.include?(uid) ? entry[:cn].first : nil + end.compact end end - diff --git a/lib/ldap_fluff/generic.rb b/lib/ldap_fluff/generic.rb index 2cef378..a5b80c2 100644 --- a/lib/ldap_fluff/generic.rb +++ b/lib/ldap_fluff/generic.rb @@ -1,117 +1,167 @@ +# frozen_string_literal: true + +# @abstract class LdapFluff::Generic + # @!attribute [rw] ldap + # @return [Net::LDAP] + # @!attribute [rw] member_service + # @return [LdapFluff::GenericMemberService] attr_accessor :ldap, :member_service - def initialize(config = {}) - @ldap = Net::LDAP.new(:host => config.host, - :base => config.base_dn, - :port => config.port, - :encryption => config.encryption, - :instrumentation_service => config.instrumentation_service) - @bind_user = config.service_user - @bind_pass = config.service_pass - @anon = config.anon_queries - @attr_login = config.attr_login - @base = config.base_dn - @group_base = (config.group_base.empty? ? config.base_dn : config.group_base) - @use_netgroups = config.use_netgroups + # @return [LdapFluff::Config] + attr_reader :config + + # @param [LdapFluff::Config] config + def initialize(config) + @config = config + @is_bind_dn = /(?] def groups_for_uid(uid) service_bind - @member_service.find_user_groups(uid) + member_service.find_user_groups(uid) rescue self.class::MemberService::UIDNotFoundException - return [] + [] end + # @param [String] gid + # @return [Array] def users_for_gid(gid) - return [] unless group_exists?(gid) - search = @member_service.find_group(gid).last + service_bind + begin + # @type [Net::LDAP::Entry] + search = member_service.find_group(gid, false) + rescue self.class::MemberService::GIDNotFoundException + return [] + end + method = select_member_method(search) - return [] if method.nil? - users_from_search_results(search, method) + method ? users_from_search_results(search, method) : [] end # returns whether a user is a member of ALL or ANY particular groups - # note: this method is much faster than groups_for_uid - # - # gids should be an array of group common names + # @note this method is much faster than groups_for_uid # - # returns true if owner is in ALL of the groups if all=true, otherwise - # returns true if owner is in ANY of the groups - def is_in_groups(uid, gids = [], all = true) + # @param [String] uid + # @param [Array] gids should be an array of group common names + # @return [Boolean] + # returns true if owner is in ALL of the groups if all=true, otherwise + # returns true if owner is in ANY of the groups + def user_in_groups?(uid, gids = [], all = true) service_bind - groups = @member_service.find_user_groups(uid).sort - gids = gids.sort - if all - return groups & gids == gids - else - return (groups & gids).any? - end - end + return true if !gids || gids.empty? - def includes_cn?(cn) - filter = Net::LDAP::Filter.eq('cn', cn) - @ldap.search(:base => @ldap.base, :filter => filter).present? + groups = member_service.find_user_groups(uid) + intersection = gids & (groups || []) + + all ? (intersection.sort == gids.sort) : !intersection.empty? end + # @raise [UnauthenticatedException] def service_bind - unless @anon || bind?(@bind_user, @bind_pass, :search => false) - raise UnauthenticatedException, - "Could not bind to #{class_name} user #{@bind_user}" - end + return if config.anon_queries || bind?(config.service_user, config.service_pass, search: false) + + raise UnauthenticatedException, "Could not bind to #{class_name} user #{config.service_user}" + end + + # @param [String] uid + # @param [String] password + # @param [Hash] opts + # @return [Boolean] + def bind?(uid = nil, password = nil, opts = {}) + uid = get_bind_dn(uid, opts) if uid && !@is_bind_dn.match(uid) + + ldap.auth(uid, password) + ldap.bind end private + + # @param [String] uid + # @param [Hash] opts + # @return [String] + def get_bind_dn(uid, opts = {}) + user = + if opts[:search] != false && uid != config.service_user + service_bind + member_service.find_user(uid, true) + end + + user ? user.dn : format(config.bind_dn_format, uid) + end + + # @param [Net::LDAP::Entry] search_result + # @return [Symbol] def select_member_method(search_result) - if @use_netgroups + return nil unless search_result + + if config.use_netgroups :nisnetgrouptriple else [:member, :memberuid, :uniquemember].find { |m| search_result.respond_to? m } end end + # @param [LdapFluff::Config] config + # @return [Net::LDAP] + def create_ldap_client(config) + Net::LDAP.new( + host: config.host, + port: config.port, + base: config.base_dn, + encryption: config.encryption, + instrumentation_service: config.instrumentation_service + ) + end + + # @param [LdapFluff::Config] config + # @return [LdapFluff::GenericMemberService] def create_member_service(config) - if @use_netgroups + if config.use_netgroups self.class::NetgroupMemberService.new(@ldap, config) else self.class::MemberService.new(@ldap, config) end end + # @return [String] def class_name self.class.name.split('::').last end + # @param [Net::LDAP::Entry] search + # @param [Symbol] method + # @return [Array] + # @abstract def users_from_search_results(search, method) - members = search.send method - if method == :memberuid - # memberuid contains an array ['user1','user2'], no need to parse it - members - elsif method == :nisnetgrouptriple - @member_service.get_netgroup_users(members) - else - @member_service.get_logins(members) - end + raise NotImplementedError, "#{search.inspect}, #{method.inspect}" end class UnauthenticatedException < LdapFluff::Error end end - diff --git a/lib/ldap_fluff/generic_member_service.rb b/lib/ldap_fluff/generic_member_service.rb index 78c4cdb..f486abe 100644 --- a/lib/ldap_fluff/generic_member_service.rb +++ b/lib/ldap_fluff/generic_member_service.rb @@ -1,85 +1,134 @@ -require 'net/ldap' +# frozen_string_literal: true +# @abstract class LdapFluff::GenericMemberService - + # @return [Net::LDAP] attr_accessor :ldap + # @!attribute [r] config + # @return [LdapFluff::Config] + # @!attribute [r] search_filter + # @return [Net::LDAP::Filter] + attr_reader :config, :search_filter + + # @param [Net::LDAP] ldap + # @param [LdapFluff::Config] config def initialize(ldap, config) - @ldap = ldap - @base = config.base_dn - @group_base = (config.group_base.empty? ? config.base_dn : config.group_base) - @search_filter = nil - begin - @search_filter = Net::LDAP::Filter.construct(config.search_filter) unless (config.search_filter.nil? || config.search_filter.empty?) - rescue Net::LDAP::LdapError => error - puts "Search filter unavailable - #{error}" - end + @ldap = ldap + @config = config + + @search_filter = try_create_filter(config.search_filter) + @login_attributes = [config.attr_login, 'uid', 'cn'] + end + + # @param [String] filter + # @return [Net::LDAP::Filter] + def try_create_filter(filter, kind = :Search) + !filter || filter.empty? ? nil : Net::LDAP::Filter.construct(filter) + rescue Net::LDAP::Error => e + warn "#{kind} filter unavailable - #{e}" + nil end - def find_user(uid) - user = @ldap.search(:filter => name_filter(uid)) - raise self.class::UIDNotFoundException if (user.nil? || user.empty?) - user + # @param [String] uid + # @return [Array, Net::LDAP::Entry] + # @raise [UIDNotFoundException] + def find_user(uid, only = nil) + # @type [Array] + user = ldap.search(filter: name_filter(uid)) + raise self.class::UIDNotFoundException if !user || user.empty? + + return_one_or_all(user, only) end - def find_by_dn(dn) + # @param [String] dn + # @return [Array, Net::LDAP::Entry] + # @raise [UIDNotFoundException] + def find_by_dn(dn, only = nil) + # @type [String] entry entry, base = dn.split(/(? name_filter(entry_value, entry_attr), :base => base) - raise self.class::UIDNotFoundException if (user.nil? || user.empty?) - user + + # @type [Array] + user = ldap.search(filter: name_filter(entry_value, entry_attr), base: base) + raise self.class::UIDNotFoundException if !user || user.empty? + + return_one_or_all(user, only) end - def find_group(gid) - group = @ldap.search(:filter => group_filter(gid), :base => @group_base) - raise self.class::GIDNotFoundException if (group.nil? || group.empty?) - group + # @param [String] gid + # @return [Array, Net::LDAP::Entry] + # @raise [UIDNotFoundException] + def find_group(gid, only = nil) + # @type [Array] + group = ldap.search(filter: group_filter(gid), base: config.group_base) + raise self.class::GIDNotFoundException if !group || group.empty? + + return_one_or_all(group, only) + end + + # @param [String] uid + # @return [Array] + # @abstract + def find_user_groups(uid) + raise NotImplementedError, uid.inspect end - def name_filter(uid, attr = @attr_login) + # @param [String] uid + # @return [Net::LDAP::Filter] + def name_filter(uid, attr = config.attr_login) filter = Net::LDAP::Filter.eq(attr, uid) + search_filter ? (filter & search_filter) : filter + end - if @search_filter.nil? - filter - else - filter & @search_filter - end + # @param [String] gid + # @return [Net::LDAP::Filter] + def group_filter(gid, attr = 'cn') + Net::LDAP::Filter.eq(attr, gid) end - def group_filter(gid) - Net::LDAP::Filter.eq("cn", gid) + # @param [String] name + # @return [Net::LDAP::Filter] + def class_filter(name) + Net::LDAP::Filter.eq('objectClass', name) end # extract the group names from the LDAP style response, - # return string will be something like - # CN=bros,OU=bropeeps,DC=jomara,DC=redhat,DC=com - def get_groups(grouplist) - grouplist.map(&:downcase).collect { |g| g.sub(/.*?cn=(.*?),.*/, '\1') } + # @param [Array] grouplist + # @return [Array] will be something like CN=bros,OU=bropeeps,DC=jomara,DC=redhat,DC=com + def get_groups(grouplist, attr = 'cn') + grouplist.map { |g| g.sub(/.*?\b#{attr}=([^,]*).*/i, '\1') } end + # @param [Array] netgroup_triples + # @return [Array] def get_netgroup_users(netgroup_triples) - return [] if netgroup_triples.nil? - netgroup_triples.map { |m| m.split(',')[1] } + netgroup_triples ? netgroup_triples.map { |m| m.split(',')[1] } : [] end + # @param [Array] userlist + # @return [Array] def get_logins(userlist) - userlist.map(&:downcase!) - [@attr_login, 'uid', 'cn'].map do |attribute| - logins = userlist.collect { |g| g.sub(/.*?#{attribute}=(.*?),.*/, '\1') } - if logins == userlist - nil - else - logins - end - end.uniq.compact.flatten + userlist.map { |g| g.sub(/.*?\b(?:#{@login_attributes.join('|')})=([^,]*).*/i, '\1') } end + # @param [Net::LDAP::Entry] entry + # @return [String] def get_login_from_entry(entry) - [@attr_login, 'uid', 'cn'].each do |attribute| + @login_attributes.each do |attribute| return entry.send(attribute) if entry.respond_to? attribute end + nil end + protected + + # @param [Array] arr + def return_one_or_all(arr, only = nil) + return arr if only.nil? + + only ? arr.first : arr.last + end end diff --git a/lib/ldap_fluff/ldap_fluff.rb b/lib/ldap_fluff/ldap_fluff.rb deleted file mode 100644 index 987fce8..0000000 --- a/lib/ldap_fluff/ldap_fluff.rb +++ /dev/null @@ -1,100 +0,0 @@ -require 'rubygems' -require 'net/ldap' - -class LdapFluff - attr_accessor :ldap, :instrumentation_service - - def initialize(config = {}) - config = LdapFluff::Config.new(config) - case config.server_type - when :posix - @ldap = Posix.new(config) - when :active_directory - @ldap = ActiveDirectory.new(config) - when :free_ipa - @ldap = FreeIPA.new(config) - else - raise 'unknown server_type' - end - @instrumentation_service = config.instrumentation_service - end - - def authenticate?(uid, password) - instrument('authenticate.ldap_fluff', :uid => uid) do |payload| - if password.nil? || password.empty? - false - else - !!@ldap.bind?(uid, password) - end - end - end - - def test - instrument('test.ldap_fluff') do |payload| - @ldap.ldap.open {} - end - end - - # return a list[] of users for a given gid - def user_list(gid) - instrument('user_list.ldap_fluff', :gid => gid) do |payload| - @ldap.users_for_gid(gid) - end - end - - # return a list[] of groups for a given uid - def group_list(uid) - instrument('group_list.ldap_fluff', :uid => uid) do |payload| - @ldap.groups_for_uid(uid) - end - end - - # return true if a user is in all of the groups - # in grouplist - def is_in_groups?(uid, grouplist) - instrument('is_in_groups?.ldap_fluff', :uid => uid, :grouplist => grouplist) do |payload| - @ldap.is_in_groups(uid, grouplist, true) - end - end - - # return true if uid exists - def valid_user?(uid) - instrument('valid_user?.ldap_fluff', :uid => uid) do |payload| - @ldap.user_exists? uid - end - end - - # return true if group exists - def valid_group?(gid) - instrument('valid_group?.ldap_fluff', :gid => gid) do |payload| - @ldap.group_exists? gid - end - end - - # return ldap entry - def find_user(uid) - instrument('find_user.ldap_fluff', :uid => uid) do |payload| - @ldap.member_service.find_user(uid) - end - end - - # return ldap entry - def find_group(gid) - instrument('find_group.ldap_fluff', :gid => gid) do |payload| - @ldap.member_service.find_group(gid) - end - end - - private - - def instrument(event, payload = {}) - payload = (payload || {}).dup - if instrumentation_service - instrumentation_service.instrument(event, payload) do |payload| - payload[:result] = yield(payload) if block_given? - end - else - yield(payload) if block_given? - end - end -end diff --git a/lib/ldap_fluff/posix.rb b/lib/ldap_fluff/posix.rb index 84612f7..b71291a 100644 --- a/lib/ldap_fluff/posix.rb +++ b/lib/ldap_fluff/posix.rb @@ -1,39 +1,45 @@ -class LdapFluff::Posix < LdapFluff::Generic +# frozen_string_literal: true - def bind?(uid = nil, password = nil, opts = {}) - unless uid.include?(',') || opts[:search] == false - service_bind - user = @member_service.find_user(uid) - uid = user.first.dn if user && user.first - end - @ldap.auth(uid, password) - @ldap.bind +class LdapFluff::Posix < LdapFluff::Generic + # @param [LdapFluff::Config] config + def initialize(config) + config.bind_dn_format ||= "uid=%s,ou=users,#{config.base_dn}" + super end private - def users_from_search_results(search, method) - # To find groups in standard LDAP without group membership attributes - # we have to look for OUs or posixGroups within the current group scope, - # i.e: cn=ldapusers,ou=groups,dc=example,dc=com -> cn=myusers,cn=ldapusers,ou=gr... - - if @use_netgroups - filter = Net::LDAP::Filter.eq('objectClass', 'nisNetgroup') + # @return [Net::LDAP::Filter] + def group_class_filter + if config.use_netgroups + Net::LDAP::Filter.eq('objectClass', 'nisNetgroup') else - filter = Net::LDAP::Filter.eq('objectClass','posixGroup') | - Net::LDAP::Filter.eq('objectClass', 'organizationalunit') | - Net::LDAP::Filter.eq('objectClass', 'groupOfUniqueNames') | - Net::LDAP::Filter.eq('objectClass', 'groupOfNames') + Net::LDAP::Filter.eq('objectClass', 'posixGroup') | + Net::LDAP::Filter.eq('objectClass', 'organizationalunit') | + Net::LDAP::Filter.eq('objectClass', 'groupOfUniqueNames') | + Net::LDAP::Filter.eq('objectClass', 'groupOfNames') end - groups = @ldap.search(:base => search.dn, :filter => filter) + end + + # To find groups in standard LDAP without group membership attributes + # we have to look for OUs or posixGroups within the current group scope, + # i.e: cn=ldapusers,ou=groups,dc=example,dc=com -> cn=myusers,cn=ldapusers,ou=gr... + # + # @param [Net::LDAP::Entry] search + # @param [Symbol] method + # @return [Array] + def users_from_search_results(search, method) + groups = ldap.search(base: search.dn, filter: group_class_filter) members = groups.map { |group| group.send(method) }.flatten.uniq - if method == :memberuid + case method + when :memberuid + # memberuid contains an array ['user1','user2'], no need to parse it members - elsif method == :nisnetgrouptriple - @member_service.get_netgroup_users(members) + when :nisnetgrouptriple + member_service.get_netgroup_users(members) else - @member_service.get_logins(members) + member_service.get_logins(members) end end end diff --git a/lib/ldap_fluff/posix_member_service.rb b/lib/ldap_fluff/posix_member_service.rb index e699b52..6256ce4 100644 --- a/lib/ldap_fluff/posix_member_service.rb +++ b/lib/ldap_fluff/posix_member_service.rb @@ -1,48 +1,41 @@ -require 'net/ldap' +# frozen_string_literal: true -# handles the naughty bits of posix ldap +# handles the naughty bits of POSIX LDAP class LdapFluff::Posix::MemberService < LdapFluff::GenericMemberService - + # @param [Net::LDAP] ldap + # @param [LdapFluff::Config] config def initialize(ldap, config) - @attr_login = (config.attr_login || 'memberuid') + config.attr_login ||= 'uid' + config.attr_member ||= 'memberuid' super end - def find_user(uid, base_dn = @base) - user = @ldap.search(:filter => name_filter(uid), :base => base_dn) - raise UIDNotFoundException if (user.nil? || user.empty?) - user - end - - # return an ldap user with groups attached - # note : this method is not particularly fast for large ldap systems - def find_user_groups(uid) - groups = [] - @ldap.search(:filter => Net::LDAP::Filter.eq('memberuid', uid), :base => @group_base).each do |entry| - groups << entry[:cn][0] + # @param [String] uid + # @return [Array, Net::LDAP::Entry] + # @raise [UIDNotFoundException] + def find_user(uid, only = nil, base_dn = nil) + if only.is_a?(String) + base_dn ||= only + only = nil + else + base_dn ||= config.base_dn end - groups - end - def times_in_groups(uid, gids, all) - filters = [] - gids.each do |cn| - filters << group_filter(cn) - end - group_filters = merge_filters(filters, all) - filter = name_filter(uid) & group_filters - @ldap.search(:base => @group_base, :filter => filter).size + # @type [Array] + user = ldap.search(filter: name_filter(uid), base: base_dn) + raise UIDNotFoundException if !user || user.empty? + + return_one_or_all(user, only) end - # AND or OR all of the filters together - def merge_filters(filters = [], all = false) - if !filters.nil? && filters.size >= 1 - filter = filters[0] - filters[1..(filters.size - 1)].each do |gfilter| - filter = (all ? filter & gfilter : filter | gfilter) - end - return filter - end + # @param [String] uid + # @return [Array] an LDAP user with groups attached + # @note this method is not particularly fast for large LDAP systems + def find_user_groups(uid) + groups = ldap.search(filter: group_filter(uid, config.attr_member), base: config.group_base) + return [] unless groups + + groups.map { |entry| entry[:cn].first } end class UIDNotFoundException < LdapFluff::Error @@ -50,5 +43,4 @@ class UIDNotFoundException < LdapFluff::Error class GIDNotFoundException < LdapFluff::Error end - end diff --git a/lib/ldap_fluff/posix_netgroup_member_service.rb b/lib/ldap_fluff/posix_netgroup_member_service.rb index 2065279..335fbb3 100644 --- a/lib/ldap_fluff/posix_netgroup_member_service.rb +++ b/lib/ldap_fluff/posix_netgroup_member_service.rb @@ -1,16 +1,17 @@ -require 'net/ldap' +# frozen_string_literal: true -# handles the naughty bits of posix ldap +# handles the naughty bits of POSIX LDAP class LdapFluff::Posix::NetgroupMemberService < LdapFluff::Posix::MemberService - - # return list of group CNs for a user + # @param [String] uid + # @return [Array] list of group CNs for a user def find_user_groups(uid) - groups = [] - @ldap.search(:filter => Net::LDAP::Filter.eq('objectClass', 'nisNetgroup'), :base => @group_base).each do |entry| + groups = ldap.search(filter: class_filter('nisNetgroup'), base: config.group_base) + return [] unless groups + + groups.map do |entry| members = get_netgroup_users(entry[:nisnetgrouptriple]) - groups << entry[:cn][0] if members.include? uid - end - groups - end + members.include?(uid) ? entry[:cn].first : nil + end.compact + end end diff --git a/test/ad_member_services_test.rb b/test/ad_member_services_test.rb index c58afc8..b9df258 100644 --- a/test/ad_member_services_test.rb +++ b/test/ad_member_services_test.rb @@ -1,109 +1,108 @@ -require 'lib/ldap_test_helper' +# frozen_string_literal: true + +require_relative 'ldap_test_helper' class TestADMemberService < MiniTest::Test include LdapTestHelper def setup super - @adms = LdapFluff::ActiveDirectory::MemberService.new(@ldap, @config) - @gfilter = group_filter('group') & group_class_filter + # noinspection RubyYardParamTypeMatch + @adms = LdapFluff::ActiveDirectory::MemberService.new(ldap, config) end def basic_user - @ldap.expect(:search, ad_user_payload, [:filter => ad_name_filter("john")]) - @ldap.expect(:search, ad_parent_payload(1), [:base => ad_group_dn, :scope => 0, :attributes => ['memberof']]) + ldap.expect(:search, ad_user_payload, [filter: ad_name_filter('john')]) + ldap.expect(:search, ad_parent_payload(1), [base: ad_group_dn, scope: 0, attributes: ['memberof']]) + @adms.ldap = ldap end def basic_group - @ldap.expect(:search, ad_group_payload, [:filter => ad_group_filter("broze"), :base => @config.group_base]) + ldap.expect(:search, ad_group_payload, [filter: ad_group_filter('broze'), base: config.group_base]) + @adms.ldap = ldap end - def nest_deep(n) + def nest_deep(num, ret_method = :ad_parent_payload) # add all the expects - 1.upto(n-1) do |i| - @ldap.expect(:search, ad_parent_payload(i + 1), [:base => ad_group_dn("bros#{i}"), :scope => 0, :attributes => ['memberof']]) + 1.upto(num - 1) do |i| + ldap.expect(:search, send(ret_method, i + 1), [base: ad_group_dn("bros#{i}"), scope: 0, attributes: ['memberof']]) end + # terminate or we loop FOREVER - @ldap.expect(:search, [], [:base => ad_group_dn("bros#{n}"), :scope => 0, :attributes => ['memberof']]) + ldap.expect(:search, [], [base: ad_group_dn("bros#{num}"), scope: 0, attributes: ['memberof']]) end - def double_nested(n) - # add all the expects - 1.upto(n - 1) do |i| - @ldap.expect(:search, ad_double_payload(i + 1), [:base => ad_group_dn("bros#{i}"), :scope => 0, :attributes => ['memberof']]) - end - # terminate or we loop FOREVER - @ldap.expect(:search, [], [:base => ad_group_dn("bros#{n}"), :scope => 0, :attributes => ['memberof']]) - (n - 1).downto(1) do |j| - @ldap.expect(:search, [], [:base => ad_group_dn("broskies#{j + 1}"), :scope => 0, :attributes => ['memberof']]) + def double_nested(num) + nest_deep(num, :ad_double_payload) + + (num - 1).downto(1) do |j| + ldap.expect(:search, [], [base: ad_group_dn("broskies#{j + 1}"), scope: 0, attributes: ['memberof']]) end end def test_find_user basic_user - @ldap.expect(:search, [], [:base => ad_group_dn('bros1'), :scope => 0, :attributes => ['memberof']]) - @adms.ldap = @ldap - assert_equal(%w(group bros1), @adms.find_user_groups("john")) - @ldap.verify + ldap.expect(:search, [], [base: ad_group_dn('bros1'), scope: 0, attributes: ['memberof']]) + + assert_equal(%w[group bros1], @adms.find_user_groups('john')) end def test_nested_groups basic_user # basic user is memberof 'group'... and 'group' is memberof 'bros1' # now make 'bros1' be memberof 'group' again - @ldap.expect(:search, ad_user_payload, [:base => ad_group_dn('bros1'), :scope => 0, :attributes => ['memberof']]) - @adms.ldap = @ldap - assert_equal(%w(group bros1), @adms.find_user_groups("john")) - @ldap.verify + ldap.expect(:search, ad_user_payload, [base: ad_group_dn('bros1'), scope: 0, attributes: ['memberof']]) + + assert_equal(%w[group bros1], @adms.find_user_groups('john')) end def test_missing_user - @ldap.expect(:search, nil, [:filter => ad_name_filter("john")]) - @adms.ldap = @ldap + ldap.expect(:search, nil, [filter: ad_name_filter('john')]) + @adms.ldap = ldap + assert_raises(LdapFluff::ActiveDirectory::MemberService::UIDNotFoundException) do - @adms.find_user_groups("john").data + @adms.find_user_groups('john').data end - @ldap.verify end def test_some_deep_recursion basic_user nest_deep(25) - @adms.ldap = @ldap + assert_equal(26, @adms.find_user_groups('john').size) - @ldap.verify end def test_complex_recursion basic_user double_nested(5) - @adms.ldap = @ldap + assert_equal(10, @adms.find_user_groups('john').size) - @ldap.verify end def test_nil_payload - assert_equal([], @adms._groups_from_ldap_data(nil)) + assert_equal([], @adms.send(:groups_from_ldap_data, nil)) end def test_empty_user - @ldap.expect(:search, [], [:filter => ad_name_filter("john")]) - @adms.ldap = @ldap + ldap.expect(:search, [], [filter: ad_name_filter('john')]) + @adms.ldap = ldap + assert_raises(LdapFluff::ActiveDirectory::MemberService::UIDNotFoundException) do - @adms.find_user_groups("john").data + @adms.find_user_groups('john').data end - @ldap.verify end def test_find_good_user - basic_user - @adms.ldap = @ldap - assert_equal(ad_user_payload, @adms.find_user('john')) + ldap.expect(:search, user = ad_user_payload, [filter: ad_name_filter('john')]) + @adms.ldap = ldap + + assert_equal(user.dup, @adms.find_user('john')) end def test_find_missing_user - @ldap.expect(:search, nil, [:filter => ad_name_filter("john")]) - @adms.ldap = @ldap + ldap.expect(:search, nil, [filter: ad_name_filter('john')]) + @adms.ldap = ldap + assert_raises(LdapFluff::ActiveDirectory::MemberService::UIDNotFoundException) do @adms.find_user('john') end @@ -111,48 +110,49 @@ def test_find_missing_user def test_find_good_group basic_group - @adms.ldap = @ldap assert_equal(ad_group_payload, @adms.find_group('broze')) end def test_find_missing_group - @ldap.expect(:search, nil, [:filter => ad_group_filter("broze"), :base => @config.group_base]) - @adms.ldap = @ldap + ldap.expect(:search, nil, [filter: ad_group_filter('broze'), base: config.group_base]) + @adms.ldap = ldap + assert_raises(LdapFluff::ActiveDirectory::MemberService::GIDNotFoundException) do @adms.find_group('broze') end end def test_find_by_dn - @ldap.expect(:search, [:result], [:filter => Net::LDAP::Filter.eq('cn', 'Foo Bar'), :base => 'dc=example,dc=com']) - @adms.ldap = @ldap + ldap.expect(:search, [:result], [filter: ad_group_filter('Foo Bar'), base: 'dc=example,dc=com']) + @adms.ldap = ldap + assert_equal([:result], @adms.find_by_dn('cn=Foo Bar,dc=example,dc=com')) - @ldap.verify end + # In at least one AD installation, users who have commas in their CNs are + # returned by the server in answer to a group membership query with + # backslashes before the commas in the CNs. Such escaped commas should not + # be used when splitting the DN. def test_find_by_dn_comma_in_cn - # In at least one AD installation, users who have commas in their CNs are - # returned by the server in answer to a group membership query with - # backslashes before the commas in the CNs. Such escaped commas should not - # be used when splitting the DN. - @ldap.expect(:search, [:result], [:filter => Net::LDAP::Filter.eq('cn', 'Bar, Foo'), :base => 'dc=example,dc=com']) - @adms.ldap = @ldap + ldap.expect(:search, [:result], [filter: ad_group_filter('Bar, Foo'), base: 'dc=example,dc=com']) + @adms.ldap = ldap + assert_equal([:result], @adms.find_by_dn('cn=Bar\, Foo,dc=example,dc=com')) - @ldap.verify end def test_find_by_dn_missing_entry - @ldap.expect(:search, nil, [:filter => Net::LDAP::Filter.eq('cn', 'Foo Bar'), :base => 'dc=example,dc=com']) - @adms.ldap = @ldap + ldap.expect(:search, nil, [filter: ad_group_filter('Foo Bar'), base: 'dc=example,dc=com']) + @adms.ldap = ldap + assert_raises(LdapFluff::ActiveDirectory::MemberService::UIDNotFoundException) do @adms.find_by_dn('cn=Foo Bar,dc=example,dc=com') end - @ldap.verify end def test_get_login_from_entry entry = Net::LDAP::Entry.new('Example User') - entry['sAMAccountName'] = 'example' + entry[:sAMAccountName] = 'example' + assert_equal(['example'], @adms.get_login_from_entry(entry)) end @@ -160,5 +160,4 @@ def test_get_login_from_entry_missing_attr entry = Net::LDAP::Entry.new('Example User') assert_nil(@adms.get_login_from_entry(entry)) end - end diff --git a/test/ad_test.rb b/test/ad_test.rb index 28ebf1d..e6919d6 100644 --- a/test/ad_test.rb +++ b/test/ad_test.rb @@ -1,80 +1,72 @@ -require 'lib/ldap_test_helper' +# frozen_string_literal: true + +require_relative 'ldap_test_helper' class TestAD < MiniTest::Test include LdapTestHelper def setup super - @ad = LdapFluff::ActiveDirectory.new(@config) + @ad = LdapFluff::ActiveDirectory.new(config) end - # default setup for service bind users - def service_bind - @ldap.expect(:auth, nil, %w(service pass)) - super + def service_bind(user = nil, pass = nil, ret = true) + super(user || "service@#{CONFIG_HASH[:host]}", pass || 'pass', ret) end def test_good_bind # no expectation on the service account - @ldap.expect(:auth, nil, ['EXAMPLE\\internet', "password"]) - @ldap.expect(:bind, true) - @ad.ldap = @ldap - assert_equal(@ad.bind?('EXAMPLE\\internet', 'password'), true) - @ldap.verify + service_bind('EXAMPLE\\internet', 'password') + + assert @ad.bind?('EXAMPLE\\internet', 'password') end def test_good_bind_with_dn # no expectation on the service account - @ldap.expect(:auth, nil, [ad_user_dn('Internet User'), "password"]) - @ldap.expect(:bind, true) - @ad.ldap = @ldap - assert_equal(@ad.bind?(ad_user_dn('Internet User'), 'password'), true) - @ldap.verify + service_bind(user = ad_user_dn('Internet User'), 'password') + + assert @ad.bind?(user.dup, 'password') end + # looks up the account name's full DN via the service account def test_good_bind_with_account_name - # looks up the account name's full DN via the service account - @md = MiniTest::Mock.new - user_result = MiniTest::Mock.new - user_result.expect(:dn, ad_user_dn('Internet User')) - @md.expect(:find_user, [user_result], %w(internet)) - @ad.member_service = @md + user_result.expect(:dn, user = ad_user_dn('Internet User')) + md.expect(:find_user, user_result, ['internet', true]) + @ad.member_service = md + service_bind - @ldap.expect(:auth, nil, [ad_user_dn('Internet User'), "password"]) - @ldap.expect(:bind, true) - assert_equal(@ad.bind?('internet', 'password'), true) - @ldap.verify + service_bind(user.dup, 'password') + + assert @ad.bind?('internet', 'password') end def test_bad_bind - @ldap.expect(:auth, nil, %w(EXAMPLE\\internet password)) - @ldap.expect(:bind, false) - @ad.ldap = @ldap - assert_equal(@ad.bind?("EXAMPLE\\internet", "password"), false) - @ldap.verify + service_bind('EXAMPLE\\internet', 'password', false) + + refute @ad.bind?('EXAMPLE\\internet', 'password') end def test_groups service_bind basic_user - assert_equal(@ad.groups_for_uid('john'), %w(bros)) + + assert_equal %w[bros], @ad.groups_for_uid('john') end def test_bad_user service_bind - md = MiniTest::Mock.new - md.expect(:find_user_groups, nil, %w(john)) - def md.find_user_groups(*args) - raise LdapFluff::ActiveDirectory::MemberService::UIDNotFoundException + + md.expect(:find_user_groups, nil) do |uid| + raise LdapFluff::ActiveDirectory::MemberService::UIDNotFoundException if uid == 'john' end @ad.member_service = md - assert_equal(@ad.groups_for_uid('john'), []) + + assert_equal [], @ad.groups_for_uid('john') end def test_bad_service_user - @ldap.expect(:auth, nil, %w(service pass)) - @ldap.expect(:bind, false) - @ad.ldap = @ldap + service_bind(nil, nil, false) + assert_raises(LdapFluff::ActiveDirectory::UnauthenticatedException) do @ad.groups_for_uid('john') end @@ -83,130 +75,171 @@ def test_bad_service_user def test_is_in_groups service_bind basic_user - assert_equal(@ad.is_in_groups("john", %w(bros), false), true) + + assert @ad.user_in_groups?('john', %w[bros], false) end def test_is_some_groups service_bind basic_user - assert_equal(@ad.is_in_groups("john", %w(bros buds), false), true) + + assert @ad.user_in_groups?('john', %w[bros buds], false) end def test_isnt_in_all_groups service_bind basic_user - assert_equal(@ad.is_in_groups("john", %w(bros buds), true), false) + + refute @ad.user_in_groups?('john', %w[bros buds], true) end def test_isnt_in_groups service_bind basic_user - assert_equal(@ad.is_in_groups("john", %w(broskies), false), false) + + refute @ad.user_in_groups?('john', %w[broskies], false) end def test_group_subset service_bind bigtime_user - assert_equal(@ad.is_in_groups("john", %w(broskies), true), true) + + assert @ad.user_in_groups?('john', %w[broskies], true) end def test_subgroups_in_groups_are_ignored - group = Net::LDAP::Entry.new('foremaners') - md = MiniTest::Mock.new - 2.times { md.expect(:find_group, [group], ['foremaners']) } - 2.times { service_bind } - def md.find_by_dn(dn) - raise LdapFluff::ActiveDirectory::MemberService::UIDNotFoundException - end + service_bind + + group = Net::LDAP::Entry.new('foremaners') + md.expect(:find_group, group, ['foremaners', false]) + # NOTE: md.expect(:find_by_dn, nil) { raise LdapFluff::ActiveDirectory::MemberService::UIDNotFoundException } @ad.member_service = md - assert_equal @ad.users_for_gid('foremaners'), [] - md.verify + + assert_equal [], @ad.users_for_gid('foremaners') end def test_user_exists - md = MiniTest::Mock.new - md.expect(:find_user, 'notnilluser', %w(john)) + md.expect(:find_user, 'notnilluser', %w[john]) + @ad.member_service = md service_bind + assert(@ad.user_exists?('john')) end def test_missing_user - md = MiniTest::Mock.new - md.expect(:find_user, nil, %w(john)) - def md.find_user(uid) - raise LdapFluff::ActiveDirectory::MemberService::UIDNotFoundException + md.expect(:find_user, nil) do |uid| + raise LdapFluff::ActiveDirectory::MemberService::UIDNotFoundException if uid == 'john' end + @ad.member_service = md service_bind + refute(@ad.user_exists?('john')) end def test_group_exists - md = MiniTest::Mock.new - md.expect(:find_group, 'notnillgroup', %w(broskies)) + md.expect(:find_group, 'notnillgroup', %w[broskies]) + @ad.member_service = md service_bind + assert(@ad.group_exists?('broskies')) end def test_missing_group - md = MiniTest::Mock.new - md.expect(:find_group, nil, %w(broskies)) - def md.find_group(uid) - raise LdapFluff::ActiveDirectory::MemberService::GIDNotFoundException + md.expect(:find_group, nil) do |gid| + raise LdapFluff::ActiveDirectory::MemberService::GIDNotFoundException if gid == 'broskies' end + @ad.member_service = md service_bind + refute(@ad.group_exists?('broskies')) end - def test_find_users_in_nested_groups - group = Net::LDAP::Entry.new('foremaners') - nested_group = Net::LDAP::Entry.new('katellers') - nested_user = Net::LDAP::Entry.new('testuser') + def nested_groups + group = Net::LDAP::Entry.new('foremaners') + group[:member] = ['CN=katellers,DC=corp,DC=windows,DC=com'] - group[:member] = ['CN=katellers,DC=corp,DC=windows,DC=com'] - nested_group[:cn] = ['katellers'] - nested_group[:member] = ['CN=Test User,CN=Users,DC=corp,DC=windows,DC=com'] + nested_group = Net::LDAP::Entry.new('katellers') + nested_group[:cn] = ['katellers'] nested_group[:objectclass] = ['organizationalunit'] - nested_user[:objectclass] = ['person'] + nested_group[:member] = ['CN=Test User,CN=Users,DC=corp,DC=windows,DC=com'] - md = MiniTest::Mock.new - 2.times { md.expect(:find_group, [group], ['foremaners']) } - 2.times { md.expect(:find_group, [nested_group], ['katellers']) } - 2.times { service_bind } + nested_user = Net::LDAP::Entry.new('testuser') + nested_user[:objectclass] = ['person'] - md.expect(:find_by_dn, [nested_group], ['CN=katellers,DC=corp,DC=windows,DC=com']) - md.expect(:find_by_dn, [nested_user], ['CN=Test User,CN=Users,DC=corp,DC=windows,DC=com']) - md.expect(:get_login_from_entry, 'testuser', [nested_user]) + [group, nested_group, nested_user] + end + + def bind_nested_groups(group, nested_group) + md.expect(:find_group, group, ['foremaners', false]) + md.expect(:find_group, nested_group, ['katellers', false]) @ad.member_service = md - assert_equal @ad.users_for_gid('foremaners'), ['testuser'] - md.verify + + 2.times { service_bind } end - def test_find_users_with_empty_nested_group - group = Net::LDAP::Entry.new('foremaners') - nested_group = Net::LDAP::Entry.new('katellers') - nested_user = Net::LDAP::Entry.new('testuser') + def test_find_users_in_nested_groups + group, nested_group, nested_user = nested_groups + bind_nested_groups(group, nested_group) + + md.expect(:find_by_dn, nested_group, [group[:member].first, true]) + md.expect(:find_by_dn, nested_user, [nested_group[:member].first, true]) + md.expect(:get_login_from_entry, 'testuser', [nested_user]) + + assert_equal ['testuser'], @ad.users_for_gid('foremaners') + end + def empty_nested_groups + group = Net::LDAP::Entry.new('foremaners') group[:member] = ['CN=Test User,CN=Users,DC=corp,DC=windows,DC=com', 'CN=katellers,DC=corp,DC=windows,DC=com'] + + nested_group = Net::LDAP::Entry.new('katellers') nested_group[:cn] = ['katellers'] nested_group[:objectclass] = ['organizationalunit'] nested_group[:memberof] = ['CN=foremaners,DC=corp,DC=windows,DC=com'] - nested_user[:objectclass] = ['person'] - md = MiniTest::Mock.new - 2.times { md.expect(:find_group, [group], ['foremaners']) } - 2.times { md.expect(:find_group, [nested_group], ['katellers']) } - 2.times { service_bind } + nested_user = Net::LDAP::Entry.new('testuser') + nested_user[:objectclass] = ['person'] - md.expect(:find_by_dn, [nested_user], ['CN=Test User,CN=Users,DC=corp,DC=windows,DC=com']) - md.expect(:find_by_dn, [nested_group], ['CN=katellers,DC=corp,DC=windows,DC=com']) + [group, nested_group, nested_user] + end + + def test_find_users_with_empty_nested_group + group, nested_group, nested_user = empty_nested_groups + bind_nested_groups(group, nested_group) + + md.expect(:find_by_dn, nested_user, [group[:member].first, true]) + md.expect(:find_by_dn, nested_group, [group[:member].last, true]) md.expect(:get_login_from_entry, 'testuser', [nested_user]) + + assert_equal ['testuser'], @ad.users_for_gid('foremaners') + end + + def test_non_exist_user_in_groups + service_bind + + md.expect(:find_user_groups, nil) do |uid| + raise LdapFluff::ActiveDirectory::MemberService::UIDNotFoundException if uid == 'john' + end @ad.member_service = md - assert_equal @ad.users_for_gid('foremaners'), ['testuser'] - md.verify + + refute @ad.user_in_groups?('john', [nil]) end + def test_invalid_users_for_group + service_bind + + group = Net::LDAP::Entry.new('foremaners').tap { |g| g[:uniquemember] = ['testuser'] } + md.expect(:find_group, group, ['foremaners', false]) + + md.expect(:find_by_dn, nil) do |dn, only| + raise LdapFluff::ActiveDirectory::MemberService::UIDNotFoundException if dn == 'testuser' && only + end + @ad.member_service = md + + assert_equal [], @ad.users_for_gid('foremaners') + end end diff --git a/test/config_test.rb b/test/config_test.rb index 890aa06..a90260f 100644 --- a/test/config_test.rb +++ b/test/config_test.rb @@ -1,30 +1,72 @@ -require 'lib/ldap_test_helper' +# frozen_string_literal: true + +require_relative 'ldap_test_helper' class ConfigTest < MiniTest::Test include LdapTestHelper def test_unsupported_type - assert_raises(LdapFluff::Config::ConfigError) { LdapFluff.new(config_hash.update :server_type => 'inactive_directory') } + assert_raises(LdapFluff::Config::ConfigError) { LdapFluff.new CONFIG_HASH.merge(server_type: 'inactive_directory') } end def test_load_posix - ldap = LdapFluff.new(config_hash.update :server_type => 'posix') - assert_instance_of LdapFluff::Posix, ldap.ldap + fluff = LdapFluff.new CONFIG_HASH.merge(server_type: 'posix') + assert_instance_of LdapFluff::Posix, fluff.ldap end def test_load_ad - ldap = LdapFluff.new(config_hash.update :server_type => 'active_directory') - assert_instance_of LdapFluff::ActiveDirectory, ldap.ldap + fluff = LdapFluff.new CONFIG_HASH.merge(server_type: :active_directory) + assert_instance_of LdapFluff::ActiveDirectory, fluff.ldap end def test_load_free_ipa - ldap = LdapFluff.new(config_hash.update :server_type => 'free_ipa') - assert_instance_of LdapFluff::FreeIPA, ldap.ldap + fluff = LdapFluff.new CONFIG_HASH.merge(server_type: 'free_ipa') + assert_instance_of LdapFluff::FreeIPA, fluff.ldap end def test_instrumentation_service is = Object.new - net_ldap = LdapFluff.new(config_hash.update :instrumentation_service => is).ldap.ldap + net_ldap = LdapFluff.new(CONFIG_HASH.merge(instrumentation_service: is)).ldap.ldap assert_equal is, net_ldap.send(:instrumentation_service) end + + def test_missing_keys + assert_raises(LdapFluff::Config::ConfigError) { LdapFluff.new } + end + + def test_unknown_keys + assert_raises(LdapFluff::Config::ConfigError) { LdapFluff.new CONFIG_HASH.merge(unknown_key: nil) } + end + + def test_anon_queries + fluff = LdapFluff.new CONFIG_HASH.merge('anon_queries' => true, service_user: nil) + assert fluff.ldap.user_in_groups?(nil) + end + + def test_nil_required_keys + %w[host port base_dn server_type service_user service_pass].each do |key| + assert_raises(LdapFluff::Config::ConfigError) { LdapFluff.new CONFIG_HASH.merge(key => nil) } + end + end + + def test_invalid_anon_queries_set + assert_raises(LdapFluff::Config::ConfigError) { LdapFluff.new CONFIG_HASH.merge(anon_queries: 0) } + end + + def test_nil_group_base + fluff = LdapFluff.new CONFIG_HASH.merge('group_base' => nil) + assert_equal fluff.ldap.config.base_dn, fluff.ldap.config.group_base + end + + def test_load_posix_netgroup + fluff = LdapFluff.new CONFIG_HASH.merge('server_type' => :posix, use_netgroups: 1) + assert_instance_of LdapFluff::Posix::NetgroupMemberService, fluff.ldap.member_service + end + + def test_bad_search_filter + assert_output(nil, /\bSearch filter unavailable\b/) do + fluff = LdapFluff.new CONFIG_HASH.merge('search_filter' => 'bad-filter') + assert_nil fluff.ldap.member_service.search_filter + end + end end diff --git a/test/ipa_member_services_test.rb b/test/ipa_member_services_test.rb index 52dd0bb..18fb9ec 100644 --- a/test/ipa_member_services_test.rb +++ b/test/ipa_member_services_test.rb @@ -1,55 +1,60 @@ -require 'lib/ldap_test_helper' +# frozen_string_literal: true + +require_relative 'ldap_test_helper' class TestIPAMemberService < MiniTest::Test include LdapTestHelper def setup super - @ipams = LdapFluff::FreeIPA::MemberService.new(@ldap, @config) + # noinspection RubyYardParamTypeMatch + @ipams = LdapFluff::FreeIPA::MemberService.new(ldap, config) end def basic_user - @ldap.expect(:search, ipa_user_payload, [:filter => ipa_name_filter("john")]) + ldap.expect(:search, ipa_user_payload, [filter: ipa_name_filter('john')]) end def basic_group - @ldap.expect(:search, ipa_group_payload, [:filter => ipa_group_filter("broze"), :base => @config.group_base]) + ldap.expect(:search, ipa_group_payload, [filter: ipa_group_filter('broze'), base: config.group_base]) end def test_find_user basic_user - @ipams.ldap = @ldap - assert_equal(%w(group bros), @ipams.find_user_groups("john")) - @ldap.verify + @ipams.ldap = ldap + + assert_equal(%w[group bros], @ipams.find_user_groups('john')) end def test_missing_user - @ldap.expect(:search, nil, [:filter => ipa_name_filter("john")]) - @ipams.ldap = @ldap + ldap.expect(:search, nil, [filter: ipa_name_filter('john')]) + @ipams.ldap = ldap + assert_raises(LdapFluff::FreeIPA::MemberService::UIDNotFoundException) do - @ipams.find_user_groups("john").data + @ipams.find_user_groups('john').data end - @ldap.verify end def test_no_groups entry = Net::LDAP::Entry.new - entry['memberof'] = [] - @ldap.expect(:search, [ Net::LDAP::Entry.new, entry ], [:filter => ipa_name_filter("john")]) - @ipams.ldap = @ldap + entry[:memberof] = [] + ldap.expect(:search, [Net::LDAP::Entry.new, entry], [filter: ipa_name_filter('john')]) + @ipams.ldap = ldap + assert_equal([], @ipams.find_user_groups('john')) - @ldap.verify end def test_find_good_user basic_user - @ipams.ldap = @ldap + @ipams.ldap = ldap + assert_equal(ipa_user_payload, @ipams.find_user('john')) end def test_find_missing_user - @ldap.expect(:search, nil, [:filter => ipa_name_filter("john")]) - @ipams.ldap = @ldap + ldap.expect(:search, nil, [filter: ipa_name_filter('john')]) + @ipams.ldap = ldap + assert_raises(LdapFluff::FreeIPA::MemberService::UIDNotFoundException) do @ipams.find_user('john') end @@ -57,16 +62,28 @@ def test_find_missing_user def test_find_good_group basic_group - @ipams.ldap = @ldap + @ipams.ldap = ldap + assert_equal(ipa_group_payload, @ipams.find_group('broze')) end def test_find_missing_group - @ldap.expect(:search, nil, [:filter => ipa_group_filter("broze"), :base => @config.group_base]) - @ipams.ldap = @ldap + ldap.expect(:search, nil, [filter: ipa_group_filter('broze'), base: config.group_base]) + @ipams.ldap = ldap + assert_raises(LdapFluff::FreeIPA::MemberService::GIDNotFoundException) do @ipams.find_group('broze') end end + def test_ipa_unique_groups + user = Net::LDAP::Entry.new.tap { |e| e[:memberof] = %w[cn=group,dc ipauniqueid=bros] } + ldap.expect(:search, [nil, user], [filter: ipa_name_filter('john')]) + + entry = Net::LDAP::Entry.new.tap { |e| e[:cn] = 'broze' } + ldap.expect(:search, [entry], [base: user[:memberof].last]) + @ipams.ldap = ldap + + assert_equal %w[group broze], @ipams.find_user_groups('john') + end end diff --git a/test/ipa_netgroup_member_services_test.rb b/test/ipa_netgroup_member_services_test.rb index aef04ad..0fd76e1 100644 --- a/test/ipa_netgroup_member_services_test.rb +++ b/test/ipa_netgroup_member_services_test.rb @@ -1,68 +1,67 @@ -require 'lib/ldap_test_helper' +# frozen_string_literal: true + +require_relative 'ldap_test_helper' class TestIPANetgroupMemberService < MiniTest::Test include LdapTestHelper def setup - netgroups_config super - @ipams = LdapFluff::FreeIPA::NetgroupMemberService.new(@ldap, netgroups_config) + # noinspection RubyYardParamTypeMatch + @ipams = LdapFluff::FreeIPA::NetgroupMemberService.new(ldap, netgroups_config) end def basic_user - @ldap.expect(:search, ipa_user_payload, [:filter => ipa_name_filter("john")]) + ldap.expect(:search, ipa_user_payload, [filter: ipa_name_filter('john')]) end def basic_group - @ldap.expect(:search, ipa_netgroup_payload('broze'), [:filter => ipa_group_filter("broze"), :base => @config.group_base]) + ldap.expect(:search, ipa_netgroup_payload('broze'), [filter: ipa_group_filter('broze'), base: config.group_base]) end def test_find_user basic_user - @ipams.ldap = @ldap + @ipams.ldap = ldap + assert_equal ipa_user_payload, @ipams.find_user('john') - @ldap.verify end def test_find_missing_user - @ldap.expect(:search, nil, [:filter => ipa_name_filter("john")]) - @ipams.ldap = @ldap - assert_raises(LdapFluff::FreeIPA::MemberService::UIDNotFoundException) do - @ipams.find_user('john') - end + ldap.expect(:search, nil, [filter: ipa_name_filter('john')]) + @ipams.ldap = ldap + + assert_raises(LdapFluff::FreeIPA::MemberService::UIDNotFoundException) { @ipams.find_user('john') } end def test_find_user_groups - response = ipa_netgroup_payload('bros', ['(,john,)', '(,joe,)']) - @ldap.expect(:search, response, [:filter => Net::LDAP::Filter.eq('objectClass', 'nisNetgroup'), - :base => @config.group_base]) + response = ipa_netgroup_payload('bros', %w[(,john,) (,joe,)]) + ldap.expect(:search, response, [filter: group_class_filter('nisNetgroup'), base: config.group_base]) + @ipams.ldap = ldap - @ipams.ldap = @ldap assert_equal(['bros'], @ipams.find_user_groups('john')) - @ldap.verify end def test_find_no_user_groups response = ipa_netgroup_payload('bros', ['(,joe,)']) - @ldap.expect(:search, response, [:filter => Net::LDAP::Filter.eq('objectClass', 'nisNetgroup'), - :base => @config.group_base]) - @ipams.ldap = @ldap + ldap.expect(:search, response, [filter: group_class_filter('nisNetgroup'), base: config.group_base]) + @ipams.ldap = ldap + assert_equal([], @ipams.find_user_groups('john')) - @ldap.verify end def test_find_group basic_group - @ipams.ldap = @ldap + @ipams.ldap = ldap + assert_equal(ipa_netgroup_payload('broze'), @ipams.find_group('broze')) end def test_find_missing_group - @ldap.expect(:search, nil, [:filter => ipa_group_filter("broze"), :base => @config.group_base]) - @ipams.ldap = @ldap + ldap.expect(:search, nil, [filter: ipa_group_filter('broze'), base: config.group_base]) + @ipams.ldap = ldap + assert_raises(LdapFluff::FreeIPA::MemberService::GIDNotFoundException) do @ipams.find_group('broze') end end - end diff --git a/test/ipa_test.rb b/test/ipa_test.rb index 657800c..b022d3a 100644 --- a/test/ipa_test.rb +++ b/test/ipa_test.rb @@ -1,71 +1,65 @@ -require 'lib/ldap_test_helper' +# frozen_string_literal: true + +require_relative 'ldap_test_helper' class TestIPA < MiniTest::Test include LdapTestHelper def setup super - @ipa = LdapFluff::FreeIPA.new(@config) + @ipa = LdapFluff::FreeIPA.new(config) end - # default setup for service bind users - def service_bind - @ldap.expect(:auth, nil, [ipa_user_bind('service'), "pass"]) - super + def service_bind(user = nil, pass = nil, ret = true) + super(user || ipa_user_bind('service'), pass || 'pass', ret) end + # looks up the uid's full DN via the service account def test_good_bind - # looks up the uid's full DN via the service account - @md = MiniTest::Mock.new - user_result = MiniTest::Mock.new - user_result.expect(:dn, ipa_user_bind('internet')) - @md.expect(:find_user, [user_result], %w(internet)) - @ipa.member_service = @md + user_result.expect(:dn, uid = ipa_user_bind('internet')) + md.expect(:find_user, user_result, ['internet', true]) + @ipa.member_service = md + service_bind - @ldap.expect(:auth, nil, [ipa_user_bind('internet'), "password"]) - @ldap.expect(:bind, true) - assert_equal(@ipa.bind?('internet', 'password'), true) - @ldap.verify + service_bind(uid.dup, 'password') + + assert @ipa.bind?('internet', 'password') end def test_good_bind_with_dn # no expectation on the service account - @ldap.expect(:auth, nil, [ipa_user_bind('internet'), "password"]) - @ldap.expect(:bind, true) - @ipa.ldap = @ldap - assert_equal(@ipa.bind?(ipa_user_bind('internet'), 'password'), true) - @ldap.verify + service_bind(uid = ipa_user_bind('internet'), 'password') + + assert @ipa.bind?(uid.dup, 'password') end def test_bad_bind - @ldap.expect(:auth, nil, [ipa_user_bind('internet'), "password"]) - @ldap.expect(:bind, false) - @ipa.ldap = @ldap - assert_equal(@ipa.bind?(ipa_user_bind("internet"), "password"), false) - @ldap.verify + service_bind(uid = ipa_user_bind('internet'), 'password', false) + + refute @ipa.bind?(uid.dup, 'password') end def test_groups service_bind basic_user - assert_equal(@ipa.groups_for_uid('john'), %w(bros)) + + assert_equal %w[bros], @ipa.groups_for_uid('john') end def test_bad_user service_bind - @md = MiniTest::Mock.new - @md.expect(:find_user_groups, nil, %w(john)) - def @md.find_user_groups(*args) - raise LdapFluff::FreeIPA::MemberService::UIDNotFoundException + + md.expect(:find_user_groups, nil) do |uid| + raise LdapFluff::FreeIPA::MemberService::UIDNotFoundException if uid == 'john' end - @ipa.member_service = @md - assert_equal(@ipa.groups_for_uid('john'), []) + @ipa.member_service = md + + assert_equal [], @ipa.groups_for_uid('john') end def test_bad_service_user - @ldap.expect(:auth, nil, [ipa_user_bind('service'), "pass"]) - @ldap.expect(:bind, false) - @ipa.ldap = @ldap + service_bind(nil, nil, false) + assert_raises(LdapFluff::FreeIPA::UnauthenticatedException) do @ipa.groups_for_uid('john') end @@ -74,94 +68,118 @@ def test_bad_service_user def test_is_in_groups service_bind basic_user - assert_equal(@ipa.is_in_groups("john", %w(bros), false), true) + + assert @ipa.user_in_groups?('john', %w[bros], false) end def test_is_some_groups service_bind basic_user - assert_equal(@ipa.is_in_groups("john", %w(bros buds), false), true) + + assert @ipa.user_in_groups?('john', %w[bros buds], false) end - def test_is_in_all_groupss + def test_is_in_all_groups service_bind bigtime_user - assert_equal(true, @ipa.is_in_groups("john", %w(broskies bros), true)) + + assert @ipa.user_in_groups?('john', %w[broskies bros], true) end def test_isnt_in_all_groups service_bind basic_user - assert_equal(@ipa.is_in_groups("john", %w(bros buds), true), false) + + refute @ipa.user_in_groups?('john', %w[bros buds], true) end def test_isnt_in_groups service_bind basic_user - assert_equal(@ipa.is_in_groups("john", %w(broskies), false), false) + + refute @ipa.user_in_groups?('john', %w[broskies], false) end def test_group_subset service_bind bigtime_user - assert_equal(@ipa.is_in_groups('john', %w(broskies), true), true) + + assert @ipa.user_in_groups?('john', %w[broskies], true) end def test_user_exists - @md = MiniTest::Mock.new - @md.expect(:find_user, 'notnilluser', %w(john)) - @ipa.member_service = @md + md.expect(:find_user, 'notnilluser', %w[john]) + @ipa.member_service = md service_bind + assert(@ipa.user_exists?('john')) end def test_missing_user - @md = MiniTest::Mock.new - @md.expect(:find_user, nil, %w(john)) - def @md.find_user(uid) - raise LdapFluff::FreeIPA::MemberService::UIDNotFoundException + md.expect(:find_user, nil) do |uid| + raise LdapFluff::FreeIPA::MemberService::UIDNotFoundException if uid == 'john' end - @ipa.member_service = @md + @ipa.member_service = md service_bind + refute(@ipa.user_exists?('john')) end def test_group_exists - @md = MiniTest::Mock.new - @md.expect(:find_group, 'notnillgroup', %w(broskies)) - @ipa.member_service = @md + md.expect(:find_group, 'notnillgroup', %w[broskies]) + @ipa.member_service = md service_bind + assert(@ipa.group_exists?('broskies')) end def test_missing_group - @md = MiniTest::Mock.new - @md.expect(:find_group, nil, %w(broskies)) - def @md.find_group(uid) - raise LdapFluff::FreeIPA::MemberService::GIDNotFoundException + md.expect(:find_group, nil) do |gid| + raise LdapFluff::FreeIPA::MemberService::GIDNotFoundException if gid == 'broskies' end - @ipa.member_service = @md + @ipa.member_service = md service_bind + refute(@ipa.group_exists?('broskies')) end - def test_find_users_in_nested_groups + def nested_groups group = Net::LDAP::Entry.new('gid=foremaners,cn=Groups,cn=accounts,dc=localdomain') group[:member] = ['gid=katellers,cn=Groups,cn=accounts,dc=localdomain'] - nested_group = Net::LDAP::Entry.new('gid=katellers,cn=Groups,cn=accounts,dc=localdomain') + + nested_group = Net::LDAP::Entry.new(group[:member].first) nested_group[:member] = ['uid=testuser,cn=users,cn=accounts,dc=localdomain'] - md = MiniTest::Mock.new - 2.times { md.expect(:find_group, [group], ['foremaners']) } - 2.times { md.expect(:find_group, [nested_group], ['katellers']) } + [group, nested_group] + end + + def test_find_users_in_nested_groups + group, nested_group = nested_groups + + md.expect(:find_group, group, ['foremaners', false]) + md.expect(:find_group, nested_group, ['katellers', false]) 2.times { service_bind } - md.expect(:get_logins, ['testuser'], [['uid=testuser,cn=users,cn=accounts,dc=localdomain']]) + md.expect(:get_logins, ['testuser'], [nested_group[:member]]) @ipa.member_service = md - assert_equal @ipa.users_for_gid('foremaners'), ['testuser'] - md.verify + assert_equal ['testuser'], @ipa.users_for_gid('foremaners') end -end + def test_insufficient_privileges_user + @ipa.member_service.ldap = service_bind + ldap.expect(:search, [nil], [filter: ipa_name_filter('john')]) + + assert_raises(LdapFluff::FreeIPA::UnauthenticatedException) { @ipa.groups_for_uid('john') } + end + def test_find_users_for_netgroup + config.use_netgroups = true + @ipa.member_service.ldap = service_bind + + group = Net::LDAP::Entry.new('gid=foremaners').tap { |g| g[:nisnetgrouptriple] = %w[(,john,) (,joe,)] } + ldap.expect(:search, [group], [filter: ipa_group_filter('foremaners'), base: config.group_base]) + + assert_equal %w[john joe], @ipa.users_for_gid('foremaners') + end +end diff --git a/test/ldap_test.rb b/test/ldap_test.rb index c368be6..68c69a3 100644 --- a/test/ldap_test.rb +++ b/test/ldap_test.rb @@ -1,62 +1,102 @@ -require 'lib/ldap_test_helper' +# frozen_string_literal: true + +require_relative 'ldap_test_helper' class TestLDAP < MiniTest::Test include LdapTestHelper def setup super - @fluff = LdapFluff.new(config_hash) + @fluff = LdapFluff.new(CONFIG_HASH) end def test_bind - @ldap.expect(:bind?, true, %w(john password)) - @fluff.ldap = @ldap - assert_equal(@fluff.authenticate?("john", "password"), true) - @ldap.verify + ldap.expect(:bind?, true, %w[john password]) + @fluff.ldap = ldap + + assert @fluff.authenticate?('john', 'password') end def test_groups - @ldap.expect(:groups_for_uid, %w(bros), %w(john)) - @fluff.ldap = @ldap - assert_equal(@fluff.group_list('john'), %w(bros)) - @ldap.verify + ldap.expect(:groups_for_uid, %w[bros], %w[john]) + @fluff.ldap = ldap + + assert_equal %w[bros], @fluff.group_list('john') end def test_group_membership - @ldap.expect(:is_in_groups, false, ['john', %w(broskies girlfriends), true]) - @fluff.ldap = @ldap - assert_equal(@fluff.is_in_groups?('john', %w(broskies girlfriends)), false) - @ldap.verify + ldap.expect(:user_in_groups?, false, ['john', %w[broskies girlfriends], true]) + @fluff.ldap = ldap + + refute @fluff.user_in_groups?('john', %w[broskies girlfriends]) end def test_valid_user - @ldap.expect(:user_exists?, true, %w(john)) - @fluff.ldap = @ldap + ldap.expect(:user_exists?, true, %w[john]) + @fluff.ldap = ldap + assert(@fluff.valid_user?('john')) - @ldap.verify end def test_valid_group - @ldap.expect(:group_exists?, true, %w(broskies)) - @fluff.ldap = @ldap + ldap.expect(:group_exists?, true, %w[broskies]) + @fluff.ldap = ldap + assert(@fluff.valid_group?('broskies')) - @ldap.verify end def test_invalid_group - @ldap.expect(:group_exists?, false, %w(broskerinos)) - @fluff.ldap = @ldap + ldap.expect(:group_exists?, false, %w[broskerinos]) + @fluff.ldap = ldap + refute(@fluff.valid_group?('broskerinos')) - @ldap.verify end def test_invalid_user - @ldap.expect(:user_exists?, false, ['johnny rotten']) - @fluff.ldap = @ldap + ldap.expect(:user_exists?, false, ['johnny rotten']) + @fluff.ldap = ldap + refute(@fluff.valid_user?('johnny rotten')) - @ldap.verify end -end + def test_unknown_server_type + @fluff.ldap.config.server_type = nil + assert_raises(RuntimeError) { @fluff.send(:create_provider, @fluff.ldap.config) } + end + + def test_instrument + md.expect(:instrument, ret = nil) do |event, payload, &blk| + if event == 'test.ldap_fluff' && payload == {} + blk.call(payload) + payload.key?(:result) + end + end + @fluff.instrumentation_service = md + ldap.expect(:open, ret) + @fluff.ldap.ldap = ldap + assert_nil @fluff.test + end + + def test_user_list + ldap.expect(:users_for_gid, %w[john], %w[bros]) + @fluff.ldap = ldap + + assert_equal %w[john], @fluff.user_list('bros') + end + + def test_found_user + md.expect(:find_user, user = Object.new, ['john', true]) + @fluff.ldap.member_service = md + + assert_equal user, @fluff.find_user('john', true) + end + + def test_found_group + md.expect(:find_group, group = [Object.new], ['bros', nil]) + @fluff.ldap.member_service = md + + assert_equal group, @fluff.find_group('bros') + end +end diff --git a/test/ldap_test_helper.rb b/test/ldap_test_helper.rb new file mode 100644 index 0000000..b0dbd8e --- /dev/null +++ b/test/ldap_test_helper.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +require 'ldap_fluff' + +require 'minitest/autorun' + +module LdapTestHelper + CONFIG_HASH = { + host: 'internet.com', + port: 387, + encryption: :start_tls, + base_dn: 'dc=internet,dc=com', + group_base: 'ou=group,dc=internet,dc=com', + service_user: 'service', + service_pass: 'pass', + server_type: :free_ipa + }.freeze + + MOCK_VARS = %w[ldap md user_result].freeze + + # @!method ldap + # @return [MiniTest::Mock, Net::LDAP] + # @!method md + # @return [MiniTest::Mock, LdapFluff::GenericMemberService] + # @!method user_result + # @return [MiniTest::Mock, Net::LDAP::Entry] + MOCK_VARS.each do |var| + define_method(var.to_sym) do + instance_variable_get("@#{var}") || instance_variable_set("@#{var}", (v = MiniTest::Mock.new)) || v + end + end + + def setup + MOCK_VARS.each { |var| instance_variable_set("@#{var}", nil) } + end + + def teardown + MOCK_VARS.each { |var| (v = instance_variable_get("@#{var}")) && v.verify } + end + + # @return [LdapFluff::Config] + def config(extra = {}) + @config ||= LdapFluff::Config.new CONFIG_HASH.merge(extra) + end + + # @return [LdapFluff::Config] + def netgroups_config + config(use_netgroups: true) + end + + # default setup for service bind users + def service_bind(user = 'service', pass = 'pass', ret = true) + ldap.expect(:auth, nil, [user, pass]) + ldap.expect(:bind, ret) + test_instance_variable.ldap = ldap + end + + def basic_user(ret = %w[bros]) + md.expect(:find_user_groups, ret, %w[john]) + test_instance_variable.member_service = md + end + + def bigtime_user + basic_user(%w[bros broskies]) + end + + def group_filter(cn, attr = 'cn') + Net::LDAP::Filter.eq(attr, cn) + end + + def group_class_filter(name = 'group') + Net::LDAP::Filter.eq('objectClass', name) + end + + # @!method ad_name_filter + # @return [Net::LDAP::Filter] + # @!method posix_name_filter + # @return [Net::LDAP::Filter] + # @!method ipa_name_filter + # @return [Net::LDAP::Filter] + { ad: 'samaccountname', posix: 'uid', ipa: 'uid' }.each do |key, attr| + define_method("#{key}_name_filter".to_sym) { |name| Net::LDAP::Filter.eq(attr, name) } + end + + alias ad_group_filter group_filter + alias ipa_group_filter group_filter + + def ipa_user_bind(uid) + "uid=#{uid},cn=users,cn=accounts,#{config.base_dn}" + end + + def ad_user_dn(name) + "CN=#{name},CN=Users,#{config.base_dn}" + end + + def ad_group_dn(name = 'group') + "cn=#{name},#{config.group_base}" + end + + def ad_user_payload + [{ memberof: [ad_group_dn] }] + end + + def ad_group_payload + ad_user_payload.tap { |arr| arr[0].merge!(cn: 'group') } + end + + def ad_parent_payload(num) + [{ memberof: [ad_group_dn("bros#{num}")] }] + end + + def ad_double_payload(num) + [{ memberof: [ad_group_dn("bros#{num}"), ad_group_dn("broskies#{num}")] }] + end + + def posix_user_payload(name = 'john') + [{ cn: [name] }] + end + + def posix_group_payload(name = 'broze') + posix_user_payload(name) + end + + def posix_netgroup_payload(cn, netgroups = []) + [{ cn: [cn], nisnetgrouptriple: netgroups }] + end + + def ipa_user_payload + @ipa_user_payload ||= [ + Net::LDAP::Entry.new.tap { |e| e[:cn] = 'John' }, + Net::LDAP::Entry.new.tap { |e| e[:memberof] = %w[cn=group,dc=internet,dc=com cn=bros,dc=internet,dc=com] } + ] + end + + def ipa_group_payload + [{ cn: 'group' }, { memberof: %w[cn=group,dc=internet,dc=com cn=bros,dc=internet,dc=com] }] + end + + alias ipa_netgroup_payload posix_netgroup_payload + + private + + # @return [LdapFluff::Generic] + def test_instance_variable + instance_variable_get("@#{self.class.name.sub(/^Test|Test$/, '').downcase}") + end +end diff --git a/test/lib/ldap_test_helper.rb b/test/lib/ldap_test_helper.rb deleted file mode 100644 index 18f10e2..0000000 --- a/test/lib/ldap_test_helper.rb +++ /dev/null @@ -1,140 +0,0 @@ -require 'ldap_fluff' -require 'ostruct' -require 'net/ldap' -require 'minitest/autorun' - -module LdapTestHelper - attr_accessor :group_base, :class_filter, :user - - def config_hash - { :host => "internet.com", - :port => "387", - :encryption => :start_tls, - :base_dn => "dc=internet,dc=com", - :group_base => "ou=group,dc=internet,dc=com", - :service_user => "service", - :service_pass => "pass", - :server_type => :free_ipa, - :attr_login => nil, - :search_filter => nil - } - end - - def setup - config - @ldap = MiniTest::Mock.new - end - - def config - @config ||= LdapFluff::Config.new config_hash - end - - def netgroups_config - @config ||= LdapFluff::Config.new config_hash.merge(:use_netgroups => true) - end - - def service_bind - @ldap.expect(:bind, true) - get_test_instance_variable.ldap = @ldap - end - - def basic_user - @md = MiniTest::Mock.new - @md.expect(:find_user_groups, %w(bros), %w(john)) - get_test_instance_variable.member_service = @md - end - - def bigtime_user - @md = MiniTest::Mock.new - @md.expect(:find_user_groups, %w(bros broskies), %w(john)) - get_test_instance_variable.member_service = @md - end - - def ad_name_filter(name) - Net::LDAP::Filter.eq("samaccountname", name) - end - - def ad_group_filter(name) - Net::LDAP::Filter.eq("cn", name) - end - - def ipa_name_filter(name) - Net::LDAP::Filter.eq("uid", name) - end - - def ipa_group_filter(name) - Net::LDAP::Filter.eq("cn", name) - end - - def group_filter(g) - Net::LDAP::Filter.eq("cn", g) - end - - def group_class_filter - Net::LDAP::Filter.eq("objectclass", "group") - end - - def ipa_user_bind(uid) - "uid=#{uid},cn=users,cn=accounts,#{@config.base_dn}" - end - - def ad_user_dn(name) - "CN=#{name},CN=Users,#{@config.base_dn}" - end - - def ad_group_dn(name='group') - "cn=#{name},#{@config.group_base}" - end - - def ad_user_payload - [{ :memberof => [ad_group_dn] }] - end - - def ad_group_payload - [{ :cn => "group", :memberof => [ad_group_dn] }] - end - - def ad_parent_payload(num) - [{ :memberof => [ad_group_dn("bros#{num}")] }] - end - - def ad_double_payload(num) - [{ :memberof => [ad_group_dn("bros#{num}"), ad_group_dn("broskies#{num}")] }] - end - - def posix_user_payload - [{ :cn => ["john"] }] - end - - def posix_group_payload - [{ :cn => ["broze"] }] - end - - def posix_netgroup_payload(cn, netgroups=[]) - [{ :cn => [cn], :nisnetgrouptriple => netgroups }] - end - - def ipa_user_payload - @ipa_user_payload_cache ||= begin - entry_1 = Net::LDAP::Entry.new - entry_1['cn'] = 'John' - entry_2 = Net::LDAP::Entry.new - entry_2['memberof'] = ['cn=group,dc=internet,dc=com', 'cn=bros,dc=internet,dc=com'] - [ entry_1, entry_2 ] - end - end - - def ipa_group_payload - [{ :cn => 'group' }, { :memberof => ['cn=group,dc=internet,dc=com', 'cn=bros,dc=internet,dc=com'] }] - end - - def ipa_netgroup_payload(cn, netgroups=[]) - [{ :cn => [cn], :nisnetgrouptriple => netgroups }] - end - - private - - def get_test_instance_variable - instance_variable_get("@#{self.class.to_s.underscore.split('_')[1..-1].join}") - end -end diff --git a/test/posix_member_services_test.rb b/test/posix_member_services_test.rb index ce41e24..25d0eee 100644 --- a/test/posix_member_services_test.rb +++ b/test/posix_member_services_test.rb @@ -1,70 +1,66 @@ -require 'lib/ldap_test_helper' +# frozen_string_literal: true + +require_relative 'ldap_test_helper' class TestPosixMemberService < MiniTest::Test include LdapTestHelper def setup super - @ms = LdapFluff::Posix::MemberService.new(@ldap, @config) + # noinspection RubyYardParamTypeMatch + @ms = LdapFluff::Posix::MemberService.new(ldap, config) end def test_find_user user = posix_user_payload - @ldap.expect(:search, user, [:filter => @ms.name_filter('john'), - :base => config.base_dn]) - @ms.ldap = @ldap - assert_equal posix_user_payload, @ms.find_user('john') - @ldap.verify + ldap.expect(:search, user, [filter: posix_name_filter('john'), base: config.base_dn]) + @ms.ldap = ldap + + assert_equal user.dup, @ms.find_user('john', config.base_dn) end def test_find_user_groups user = posix_group_payload - @ldap.expect(:search, user, [:filter => @ms.name_filter('john'), - :base => config.group_base]) - @ms.ldap = @ldap + ldap.expect(:search, user, [filter: group_filter('john', 'memberuid'), base: config.group_base]) + @ms.ldap = ldap + assert_equal ['broze'], @ms.find_user_groups('john') - @ldap.verify end def test_find_no_groups - @ldap.expect(:search, [], [:filter => @ms.name_filter("john"), - :base => config.group_base]) - @ms.ldap = @ldap + ldap.expect(:search, [], [filter: group_filter('john', 'memberuid'), base: config.group_base]) + @ms.ldap = ldap + assert_equal [], @ms.find_user_groups('john') - @ldap.verify end def test_user_exists user = posix_user_payload - @ldap.expect(:search, user, [:filter => @ms.name_filter('john'), - :base => config.base_dn]) - @ms.ldap = @ldap - assert @ms.find_user('john') - @ldap.verify + ldap.expect(:search, user, [filter: posix_name_filter('john'), base: config.base_dn]) + @ms.ldap = ldap + + assert_equal user.last.dup, @ms.find_user('john', false) end def test_user_doesnt_exists - @ldap.expect(:search, nil, [:filter => @ms.name_filter('john'), - :base => config.base_dn]) - @ms.ldap = @ldap + ldap.expect(:search, nil, [filter: posix_name_filter('john'), base: config.base_dn]) + @ms.ldap = ldap + assert_raises(LdapFluff::Posix::MemberService::UIDNotFoundException) { @ms.find_user('john') } - @ldap.verify end def test_group_exists group = posix_group_payload - @ldap.expect(:search, group, [:filter => @ms.group_filter('broze'), - :base => config.group_base]) - @ms.ldap = @ldap + ldap.expect(:search, group, [filter: group_filter('broze'), base: config.group_base]) + @ms.ldap = ldap + assert @ms.find_group('broze') - @ldap.verify end def test_group_doesnt_exists - @ldap.expect(:search, nil, [:filter => @ms.group_filter('broze'), - :base => config.group_base]) - @ms.ldap = @ldap + ldap.expect(:search, nil, [filter: group_filter('broze'), base: config.group_base]) + @ms.ldap = ldap + assert_raises(LdapFluff::Posix::MemberService::GIDNotFoundException) { @ms.find_group('broze') } - @ldap.verify end end diff --git a/test/posix_netgroup_member_services_test.rb b/test/posix_netgroup_member_services_test.rb index 2e51f69..e4dd6c9 100644 --- a/test/posix_netgroup_member_services_test.rb +++ b/test/posix_netgroup_member_services_test.rb @@ -1,74 +1,67 @@ -require 'lib/ldap_test_helper' +# frozen_string_literal: true + +require_relative 'ldap_test_helper' class TestPosixNetgroupMemberService < MiniTest::Test include LdapTestHelper def setup - netgroups_config super - @ms = LdapFluff::Posix::NetgroupMemberService.new(@ldap, netgroups_config) + # noinspection RubyYardParamTypeMatch + @ms = LdapFluff::Posix::NetgroupMemberService.new(ldap, netgroups_config) end def test_find_user user = posix_user_payload - @ldap.expect(:search, user, [:filter => @ms.name_filter('john'), - :base => config.base_dn]) - @ms.ldap = @ldap - assert_equal posix_user_payload, @ms.find_user('john') - @ldap.verify + ldap.expect(:search, user, [filter: posix_name_filter('john'), base: config.base_dn]) + @ms.ldap = ldap + + assert_equal user.dup, @ms.find_user('john') end def test_find_user_groups - response = posix_netgroup_payload('bros', ['(,john,)', '(,joe,)']) - @ldap.expect(:search, response, [:filter => Net::LDAP::Filter.eq('objectClass', 'nisNetgroup'), - :base => config.group_base]) + response = posix_netgroup_payload('bros', %w[(,john,) (,joe,)]) + ldap.expect(:search, response, [filter: group_class_filter('nisNetgroup'), base: config.group_base]) + @ms.ldap = ldap - @ms.ldap = @ldap assert_equal ['bros'], @ms.find_user_groups('john') - @ldap.verify end def test_find_no_user_groups response = posix_netgroup_payload('bros', ['(,joe,)']) - @ldap.expect(:search, response, [:filter => Net::LDAP::Filter.eq('objectClass', 'nisNetgroup'), - :base => config.group_base]) + ldap.expect(:search, response, [filter: group_class_filter('nisNetgroup'), base: config.group_base]) + @ms.ldap = ldap - @ms.ldap = @ldap assert_equal [], @ms.find_user_groups('john') - @ldap.verify end def test_user_exists user = posix_user_payload - @ldap.expect(:search, user, [:filter => @ms.name_filter('john'), - :base => config.base_dn]) - @ms.ldap = @ldap + ldap.expect(:search, user, [filter: posix_name_filter('john'), base: config.base_dn]) + @ms.ldap = ldap + assert @ms.find_user('john') - @ldap.verify end def test_user_doesnt_exists - @ldap.expect(:search, nil, [:filter => @ms.name_filter('john'), - :base => config.base_dn]) - @ms.ldap = @ldap + ldap.expect(:search, nil, [filter: posix_name_filter('john'), base: config.base_dn]) + @ms.ldap = ldap + assert_raises(LdapFluff::Posix::MemberService::UIDNotFoundException) { @ms.find_user('john') } - @ldap.verify end def test_group_exists group = posix_netgroup_payload('broze') - @ldap.expect(:search, group, [:filter => @ms.group_filter('broze'), - :base => config.group_base]) - @ms.ldap = @ldap + ldap.expect(:search, group, [filter: group_filter('broze'), base: config.group_base]) + @ms.ldap = ldap + assert @ms.find_group('broze') - @ldap.verify end def test_group_doesnt_exists - @ldap.expect(:search, nil, [:filter => @ms.group_filter('broze'), - :base => config.group_base]) - @ms.ldap = @ldap + ldap.expect(:search, nil, [filter: group_filter('broze'), base: config.group_base]) + @ms.ldap = ldap + assert_raises(LdapFluff::Posix::MemberService::GIDNotFoundException) { @ms.find_group('broze') } - @ldap.verify end end diff --git a/test/posix_test.rb b/test/posix_test.rb index 74dc592..5098d51 100644 --- a/test/posix_test.rb +++ b/test/posix_test.rb @@ -1,139 +1,192 @@ -require 'lib/ldap_test_helper' +# frozen_string_literal: true + +require_relative 'ldap_test_helper' class TestPosix < MiniTest::Test include LdapTestHelper def setup super - @posix = LdapFluff::Posix.new(@config) + @posix = LdapFluff::Posix.new(config) end - def service_bind - @ldap.expect(:auth, nil, %w[service pass]) - super + def service_bind(user = nil, pass = nil, ret = true) + super(user || "uid=service,ou=users,#{CONFIG_HASH[:base_dn]}", pass || 'pass', ret) end def test_groups service_bind basic_user - assert_equal(@posix.groups_for_uid("john"), %w(bros)) + + assert_equal %w[bros], @posix.groups_for_uid('john') end def test_missing_user - md = MiniTest::Mock.new - md.expect(:find_user_groups, [], %w(john)) + service_bind + + md.expect(:find_user_groups, [], %w[john]) @posix.member_service = md + assert_equal([], @posix.groups_for_uid('john')) end def test_isnt_in_groups service_bind basic_user - assert_equal(@posix.is_in_groups('john', %w(broskies), true), false) + + refute @posix.user_in_groups?('john', %w[broskies], true) end def test_is_in_groups service_bind basic_user - assert_equal(@posix.is_in_groups('john', %w(bros), true), true) + + assert @posix.user_in_groups?('john', %w[bros], true) end def test_is_in_no_groups service_bind - basic_user - assert_equal(@posix.is_in_groups('john', [], true), true) + # basic_user + + assert @posix.user_in_groups?('john', [], true) end + # looks up the uid's full DN via the service account def test_good_bind - # looks up the uid's full DN via the service account - @md = MiniTest::Mock.new - user_result = MiniTest::Mock.new user_result.expect(:dn, 'uid=internet,dn=example') - @md.expect(:find_user, [user_result], %w(internet)) - @posix.member_service = @md + md.expect(:find_user, user_result, ['internet', true]) + @posix.member_service = md + service_bind - @ldap.expect(:auth, nil, %w[uid=internet,dn=example password]) - @ldap.expect(:bind, true) - @posix.ldap = @ldap - assert_equal(@posix.bind?("internet", "password"), true) + service_bind('uid=internet,dn=example', 'password') + + assert @posix.bind?('internet', 'password') end def test_good_bind_with_dn # no expectation on the service account - @ldap.expect(:auth, nil, %w[uid=internet,dn=example password]) - @ldap.expect(:bind, true) - @posix.ldap = @ldap - assert_equal(@posix.bind?("uid=internet,dn=example", "password"), true) + service_bind('uid=internet,dn=example', 'password') + + assert @posix.bind?('uid=internet,dn=example', 'password') end def test_bad_bind - @ldap.expect(:auth, nil, %w[uid=internet,dn=example password]) - @ldap.expect(:bind, false) - @posix.ldap = @ldap - assert_equal(@posix.bind?("uid=internet,dn=example", "password"), false) + service_bind('uid=internet,dn=example', 'password', false) + + refute @posix.bind?('uid=internet,dn=example', 'password') end def test_user_exists service_bind - md = MiniTest::Mock.new - md.expect(:find_user, 'notnilluser', %w(john)) + + md.expect(:find_user, 'notnilluser', %w[john]) @posix.member_service = md + assert(@posix.user_exists?('john')) end - def test_missing_user + def test_user_doesnt_exists service_bind - md = MiniTest::Mock.new - md.expect(:find_user, nil, %w(john)) - def md.find_user(uid) - raise LdapFluff::Posix::MemberService::UIDNotFoundException + + md.expect(:find_user, nil) do |uid| + raise LdapFluff::Posix::MemberService::UIDNotFoundException if uid == 'john' end @posix.member_service = md + refute(@posix.user_exists?('john')) end def test_group_exists service_bind - md = MiniTest::Mock.new - md.expect(:find_group, 'notnillgroup', %w(broskies)) + + md.expect(:find_group, 'notnillgroup', %w[broskies]) @posix.member_service = md + assert(@posix.group_exists?('broskies')) end def test_missing_group service_bind - md = MiniTest::Mock.new - md.expect(:find_group, nil, %w(broskies)) - def md.find_group(uid) - raise LdapFluff::Posix::MemberService::GIDNotFoundException + + md.expect(:find_group, nil) do |gid| + raise LdapFluff::Posix::MemberService::GIDNotFoundException if gid == 'broskies' end @posix.member_service = md + refute(@posix.group_exists?('broskies')) end - def test_find_users_in_nested_groups + def posix_groups_filter + group_class_filter('posixGroup') | + group_class_filter('organizationalunit') | + group_class_filter('groupOfUniqueNames') | + group_class_filter('groupOfNames') + end + + def bind_nested_groups(attr = :memberuid) service_bind + group = Net::LDAP::Entry.new('CN=foremaners,DC=example,DC=com') - group[:memberuid] = ['katellers'] + group[attr] = ['katellers'] + nested_group = Net::LDAP::Entry.new('CN=katellers,CN=foremaners,DC=example,DC=com') - nested_group[:memberuid] = ['testuser'] - - @ldap.expect(:search, - [nested_group], - [{ :base => group.dn, - :filter => Net::LDAP::Filter.eq('objectClass', 'posixGroup') | - Net::LDAP::Filter.eq('objectClass', 'organizationalunit') | - Net::LDAP::Filter.eq('objectClass', 'groupOfUniqueNames') | - Net::LDAP::Filter.eq('objectClass', 'groupOfNames')}]) - @posix.ldap = @ldap - - md = MiniTest::Mock.new - 2.times { md.expect(:find_group, [group], ['foremaners']) } + nested_group[attr] = [attr == :member ? 'uid=testuser,' : 'testuser'] + + [group, nested_group] + end + + def test_find_users_in_nested_groups + group, nested_group = bind_nested_groups + ldap.expect(:search, [nested_group], [base: group.dn, filter: posix_groups_filter]) + + md.expect(:find_group, group, ['foremaners', false]) + @posix.member_service = md + + assert_equal ['testuser'], @posix.users_for_gid('foremaners') + end + + def test_find_members_in_group + group, nested_group = bind_nested_groups(:member) + ldap.expect(:search, [nested_group], [base: group.dn, filter: posix_groups_filter]) + + md.expect(:find_group, group, ['foremaners', false]) + md.expect(:get_logins, ['katellers'], [nested_group[:member]]) @posix.member_service = md - assert_equal @posix.users_for_gid('foremaners'), ['testuser'] + assert_equal ['katellers'], @posix.users_for_gid('foremaners') + end + + def test_find_users_in_netgroup + config.use_netgroups = true + + group, nested_group = bind_nested_groups(:nisnetgrouptriple) + ldap.expect(:search, [nested_group], [base: group.dn, filter: group_class_filter('nisNetgroup')]) + + md.expect(:find_group, group, ['foremaners', false]) + md.expect(:get_netgroup_users, ['katellers'], [['testuser']]) + @posix.member_service = md + + assert_equal ['katellers'], @posix.users_for_gid('foremaners') + end + + def test_users_in_non_exist_group + service_bind + + md.expect(:find_group, nil) do |gid, only| + raise LdapFluff::Posix::MemberService::GIDNotFoundException if gid == 'foremaners' && only == false + end + @posix.member_service = md + + assert_equal [], @posix.users_for_gid('foremaners') + end + + def test_users_for_group + group, nested_group = bind_nested_groups(:member) + + ldap.expect(:search, [group], [filter: group_filter('foremaners'), base: config.group_base]) + ldap.expect(:search, [nested_group], [base: group.dn, filter: posix_groups_filter]) + @posix.member_service.ldap = ldap - md.verify - @ldap.verify + assert_equal ['testuser'], @posix.users_for_gid('foremaners') end end diff --git a/travis/ldapfluff_up_to_snuff.sh b/travis/ldapfluff_up_to_snuff.sh deleted file mode 100755 index 0dc51f9..0000000 --- a/travis/ldapfluff_up_to_snuff.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -e # fail on error - -echo "" -echo "MINITEST" -bundle exec rake - -echo "" -echo "RUBOCOP" -# disable rubocop for now -bundle exec rubocop $(git ls-files | grep ".*\.rb$") Gemfile Rakefile ldap_fluff.gemspec || true