thorin/redmine_ldap_sync

View on GitHub
lib/ldap_sync/entity_manager.rb

Summary

Maintainability
D
2 days
Test Coverage
# encoding: utf-8
# Copyright (C) 2011-2013  The Redmine LDAP Sync Authors
#
# This file is part of Redmine LDAP Sync.
#
# Redmine LDAP Sync is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Redmine LDAP Sync is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Redmine LDAP Sync.  If not, see <http://www.gnu.org/licenses/>.
module LdapSync::EntityManager

  public
    def connect_as_user?; setting.account.include?('$login'); end

  private
    def get_user_fields(username, user_data=nil, options={})
      fields_to_sync = setting.user_fields_to_sync
      if options.try(:fetch, :include_required, false)
        custom_fields = user_required_custom_fields.map {|cf| cf.id.to_s }
        fields_to_sync -= (User::STANDARD_FIELDS + custom_fields)
        fields_to_sync += (User::STANDARD_FIELDS + custom_fields)
      end
      ldap_attrs_to_sync = setting.user_ldap_attrs_to_sync(fields_to_sync)

      user_data ||= with_ldap_connection do |ldap|
        find_user(ldap, username, ldap_attrs_to_sync)
      end
      return {} if user_data.nil?

      user_fields = user_data.inject({}) do |fields, (attr, value)|
        f = setting.user_field(attr)
        if f && fields_to_sync.include?(f)
          fields[f] = value.first unless value.nil? || value.first.blank?
        end
        fields
      end

      user_required_custom_fields.each do |cf|
        if user_fields[cf.id.to_s].blank?
          user_fields[cf.id.to_s] = cf.default_value
        end
      end

      user_fields
    end

    def get_group_fields(groupname, group_data = nil)
      group_data ||= with_ldap_connection do |ldap|
        find_group(ldap, groupname, [n(:groupname), *setting.group_ldap_attrs_to_sync])
      end || {}

      group_fields = group_data.inject({}) do |fields, (attr, value)|
        f = setting.group_field(attr)
        if f && setting.group_fields_to_sync.include?(f)
          fields[f] = value.first unless value.nil? || value.first.blank?
        end
        fields
      end

      group_required_custom_fields.each do |cf|
        if group_fields[cf.id.to_s].blank?
          group_fields[cf.id.to_s] = cf.default_value
        end
      end

      group_fields
    end

    def user_required_custom_fields
      @user_required_custom_fields ||= UserCustomField.select(&:is_required)
    end

    def group_required_custom_fields
      @group_required_custom_fields ||= GroupCustomField.select(&:is_required)
    end

    def ldap_users
      return @ldap_users if defined? @ldap_users

      with_ldap_connection do |ldap|
        changes = { enabled: SortedSet.new, locked: SortedSet.new, deleted: SortedSet.new }

        unless setting.has_account_flags?
          changes[:enabled] += find_all_users(ldap, n(:login)).map(&:first)
        else
          find_all_users(ldap, ns(:login, :account_flags)) do |entry|
            if account_locked?(entry[n(:account_flags)].first)
              changes[:locked] << entry[n(:login)].first
            else
              changes[:enabled] << entry[n(:login)].first
            end
          end
        end

        changes[:enabled].delete(nil)
        changes[:locked].delete(nil)

        users_on_local = self.users.active.map {|u| u.login.downcase }
        users_on_ldap = changes.values.sum.map(&:downcase)
        deleted_users = users_on_local - users_on_ldap
        changes[:deleted] = deleted_users

        trace "-- Found #{changes[:enabled].size} users active" \
          ", #{changes[:locked].size} locked" \
          " and #{changes[:deleted].size} deleted on ldap"

        # Sort users, clearer for the rake task
        # TODO user Array instead of Set at the beginning ?
        changes[:enabled] = changes[:enabled].to_a.sort
        changes[:locked] = changes[:locked].to_a.sort
        changes[:deleted] = changes[:deleted].to_a.sort

        @ldap_users = changes
      end

    end

    def groups_changes(user)
      return unless setting.active?
      changes = { added: SortedSet.new, deleted: SortedSet.new }

      user_groups = user.groups.map {|g| g.name.downcase }
      groupname_regexp = setting.groupname_regexp

      with_ldap_connection do |ldap|
        # Find which of the user's current groups are in ldap
        filtered_groups = user_groups.select {|g| groupname_regexp =~ g }
        names_filter    = filtered_groups.map {|g| Net::LDAP::Filter.eq( setting.groupname, g )}.reduce(:|)
        find_all_groups(ldap, names_filter, n(:groupname)) do |group|
          changes[:deleted] << group.first
        end if names_filter

        changes[:added] += get_primary_group(ldap, user) if setting.has_primary_group?

        case setting.group_membership
        when 'on_groups'
          # Find user's memberid
          memberid = user.login
          if setting.user_memberid != setting.login
            entry = find_user(ldap, user.login, ns(:user_memberid)) and
              memberid = entry[n(:user_memberid)].first and
              user_dn = entry[:dn].first
          end

          if setting.user_memberid == setting.login || entry.present?
            # Find the static groups to which the user belongs to (groupOfNames)
            member_filter = Net::LDAP::Filter.eq( setting.member, memberid )
            find_all_groups(ldap, member_filter, n(:groupname)) do |group|
              changes[:added] << group.first
            end if memberid
          end

        else # 'on_members'
          entry = find_user(ldap, user.login, ns(:user_groups))
          if entry.present?
            groups = entry[n(:user_groups)]
            user_dn = entry[:dn].first

            names_filter = groups.map{|g| Net::LDAP::Filter.eq( setting.groupid, g )}.reduce(:|)
            find_all_groups(ldap, names_filter, n(:groupname)) do |group|
              changes[:added] << group.first
            end if names_filter
          end
        end

        changes[:added] = changes[:added].inject(Set.new) do |closure, group|
          closure + closure_cache.fetch(group) do
            get_group_closure(ldap, group).select {|g| groupname_regexp =~ g }
          end
        end if setting.nested_groups_enabled?

        # Find the dynamic groups to which the user belongs to (groupOfURLs)
        if setting.sync_dyngroups?
          user_dn ||= find_user(ldap, user.login, :dn).try(:first)
          changes[:added] += get_dynamic_groups(user_dn) unless user_dn.nil?
        end
      end

      changes[:added].delete_if {|group| groupname_regexp !~ group }
      changes[:deleted] -= changes[:added]
      changes[:added].delete_if {|group| user_groups.include?(group.downcase) }

      changes
    ensure
      reset_parents_cache! unless running_rake?
    end

    def get_primary_group(ldap, user)
      primary_group_id = find_user(ldap, user.login, n(:primary_group)).try(:first)
      return [] if primary_group_id.nil?

      # Map GID to group name
      gid_filter = Net::LDAP::Filter.eq( setting.primary_group, primary_group_id )
      find_all_groups(ldap, gid_filter, n(:groupname)).first || []
    end

    def get_dynamic_groups(user_dn)
      reload_dyngroups! unless dyngroups_fresh?

      dyngroups_cache.fetch(member_key(user_dn)) || []
    end

    def reload_dyngroups!
      with_ldap_connection {|c| find_all_dyngroups(c, update_cache: true) }
    end

    def get_group_closure(ldap, group, closure=Set.new)
      groupname = group.is_a?(String) ? group : group[n(:groupname)].first
      parent_groups = parents_cache.fetch(groupname) do
        case setting.nested_groups
        when 'on_members'
          group = find_group(ldap, groupname, ns(:groupname, :group_memberid, :parent_group)) if group.is_a? String

          if group[n(:parent_group)].present?
            groups_filter = group[n(:parent_group)].map{|g| Net::LDAP::Filter.eq( setting.group_parentid, g )}.reduce(:|)
            cacheable_ber find_all_groups(ldap, groups_filter, ns(:groupname, :group_memberid, :parent_group))
          else
            Array.new
          end
        else # 'on_parents'
          group = find_group(ldap, groupname, ns(:groupname, :group_memberid)) if group.is_a? String

          member_filter = Net::LDAP::Filter.eq( setting.member_group, group[n(:group_memberid)].first )
          cacheable_ber find_all_groups(ldap, member_filter, ns(:groupname, :group_memberid)).map
        end
      end

      closure << groupname
      parent_groups.each_with_object(closure) do |group, closure|
        closure += get_group_closure(ldap, group, closure) unless closure.include? group[n(:groupname)].first
      end
    end

    def find_group(ldap, group_name, attrs, &block)
      extra_filter = Net::LDAP::Filter.eq( setting.groupname, group_name )
      result = find_all_groups(ldap, extra_filter, attrs, &block)
      result.first if !block_given? && result.present?
    end

    def find_all_groups(ldap, extra_filter, attrs, options = {}, &block)
      object_class = options[:class] || setting.class_group
      groups_base_dn = setting.has_groups_base_dn? ? setting.groups_base_dn : nil
      group_filter = Net::LDAP::Filter.eq( :objectclass, object_class )
      group_filter &= Net::LDAP::Filter.construct( setting.group_search_filter ) if setting.group_search_filter.present?
      group_filter &= extra_filter if extra_filter

      ldap_search(ldap, {base:  groups_base_dn,
                   filter: group_filter,
                   attributes: attrs,
                   return_result: block_given? ? false : true},
                  &block)
    end

    def find_all_dyngroups(ldap, options = {})
      options = options.reverse_merge(attrs: [n(:groupname), :member], update_cache: false)
      users_base_dn = setting.base_dn.downcase

      dyngroups = Hash.new{|h,k| h[k] = []}

      find_all_groups(ldap, nil, options[:attrs], class: 'groupOfURLs') do |entry|
        yield entry if block_given?

        if options[:update_cache]
          entry[:member].each do |member|
            next unless (member.downcase.ends_with?(users_base_dn))

            dyngroups[member_key(member)] << entry[n(:groupname)].first
          end
        end
      end

      update_dyngroups_cache!(dyngroups) if options[:update_cache]
    end

    def find_user(ldap, login, attrs, &block)
      user_filter = Net::LDAP::Filter.eq( :objectclass, setting.class_user )
      user_filter &= setting.ldap_filter if setting.filter.present?
      login_filter = Net::LDAP::Filter.eq( setting.login, login )

      result = ldap_search(ldap, {base: setting.base_dn,
                            filter: user_filter & login_filter,
                            attributes: attrs,
                            return_result: block_given? ? false : true},
                           &block)
      result.first if !block_given? && result.present?
    end

    def find_all_users(ldap, attrs, &block)
      user_filter = Net::LDAP::Filter.eq( :objectclass, setting.class_user )
      user_filter &= setting.ldap_filter if setting.filter.present?

      ldap_search(ldap, {base: setting.base_dn,
                   filter: user_filter,
                   scope: (Net::LDAP::SearchScope_SingleLevel if setting.users_search_onelevel?),
                   attributes: attrs,
                   return_result: block_given? ? false : true},
                  &block)
    end

    def ldap_search(ldap, options, &block)
      attrs = options[:attributes]

      return ldap.search(options, &block) if attrs.is_a?(Array) || attrs.nil?

      options[:attributes] = [attrs]

      block = Proc.new {|e| yield e[attrs] } if block_given?
      result = ldap.search(options, &block) or fail
      result ||= [] unless block_given?
      result.map {|e| e[attrs] } unless block_given?
    rescue => exception
      os = ldap.get_operation_result
      raise Net::LDAP::Error, "LDAP Error(#{os.code}): #{os.message}"
    end

    def n(field)
      setting.send(field)
    end

    def ns(*args)
      setting.ldap_attributes(*args)
    end

    def member_key(member)
      member[0...-setting.base_dn.length-1]
    end

    def account_locked?(flags)
      return false if flags.blank?

      !!setting.account_locked_proc.try(:call, flags)
    end

    def cacheable_ber(result)
      result.map do |h|
        h = Hash[ h.map {|k,v| [k, v.to_a] } ]
        HashWithIndifferentAccess.new( h )
      end
    end

    def with_ldap_connection(login = nil, password = nil)
      thread = Thread.current

      return yield thread[:local_ldap_con] if thread[:local_ldap_con].present?

      ldap_con = if setting.account && setting.account.include?('$login')
        initialize_ldap_con(setting.account.sub('$login', Net::LDAP::DN.escape(login)), password)
      else
        initialize_ldap_con(setting.account, setting.account_password)
      end

      ldap_con.open do |ldap|
        begin
          yield thread[:local_ldap_con] = ldap
        ensure
          thread[:local_ldap_con] = nil
        end
      end
    end

    def info(msg = ""); trace msg, level: :change; end
    def change(obj = "", msg = ""); trace msg, level: :change, obj: obj; end
    def error(msg); trace msg, level: :error; end
end