thorin/redmine_ldap_sync

View on GitHub
app/models/ldap_setting.rb

Summary

Maintainability
D
1 day
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/>.
class LdapSetting
  include Redmine::SafeAttributes
  include Redmine::I18n

  include ActiveModel::Validations
  include ActiveModel::Validations::Callbacks
  include ActiveModel::Conversion
  extend ActiveModel::Naming
  include ActiveModel::AttributeMethods

  # LDAP_DESCRIPTORS
  LDAP_ATTRIBUTES = %w( groupname member user_memberid user_groups groupid parent_group primary_group group_parentid member_group group_memberid account_flags )
  CLASS_NAMES = %w( class_user class_group )
  FLAGS = %w( create_groups create_users active )
  COMBOS = %w( group_membership nested_groups sync_on_login dyngroups users_search_scope )
  OTHERS = %w( account_locked_test user_fields_to_sync group_fields_to_sync user_ldap_attrs group_ldap_attrs fixed_group admin_group required_group group_search_filter groupname_pattern groups_base_dn dyngroups_cache_ttl )

  validates_presence_of :auth_source_ldap_id
  validates_presence_of :class_user, :class_group, :groupname
  validates_presence_of :member, :user_memberid, :if => :membership_on_groups?
  validates_presence_of :user_groups, :groupid, :if => :membership_on_members?
  validates_presence_of :parent_group, :group_parentid, :if => :nested_on_members?
  validates_presence_of :member_group, :group_memberid, :if => :nested_on_parents?
  validates_presence_of :dyngroups_cache_ttl, :if => :dyngroups_enabled_with_ttl?

  validates_inclusion_of :nested_groups, :in => ['on_members', 'on_parents', '']
  validates_inclusion_of :group_membership, :in => ['on_groups', 'on_members']
  validates_inclusion_of :sync_on_login, :in => ['user_fields', 'user_fields_and_groups', '']
  validates_inclusion_of :dyngroups, :in => ['enabled', 'enabled_with_ttl', '']
  validates_inclusion_of :users_search_scope, :in => ['onelevel', 'subtree']

  validates_format_of *(LDAP_ATTRIBUTES + [{ :with => /\A[a-z][a-z0-9-]*\z/i, :allow_blank => true }])

  validates_numericality_of :dyngroups_cache_ttl, :only_integer => true, :allow_blank => true

  validate :validate_groupname_pattern
  validate :validate_account_locked_test
  validate :validate_group_filter
  validate :validate_user_fields_to_sync, :validate_user_ldap_attrs
  validate :validate_group_fields_to_sync, :validate_group_ldap_attrs

  before_validation :strip_names, :set_ldap_attrs, :set_fields_to_sync

  delegate :base_dn, :account, :account_password, :filter, :to => :auth_source_ldap

  attribute_method_affix :prefix => 'has_', :suffix => '?'
  attribute_method_suffix '?', '='

  safe_attributes *(LDAP_ATTRIBUTES + CLASS_NAMES + FLAGS + COMBOS + OTHERS)
  define_attribute_methods LDAP_ATTRIBUTES + CLASS_NAMES + FLAGS + COMBOS + OTHERS

  [:login, *User::STANDARD_FIELDS].each {|f| module_eval("def #{f}; auth_source_ldap.attr_#{f}; end") }

  def id
    @auth_source_ldap_id
  end

  def to_key
    return nil unless persisted?
    id ? [id] : nil
  end

  def name
    auth_source_ldap.name
  end

  def active?
    return @active if defined? @active

    @active = [true, '1', 'yes'].include? active
  end

  def active=(value)
    @active = value
    @attributes[:active] = value
  end

  def nested_groups_enabled?
    self.active? && nested_groups.present?
  end

  def nested_on_members?
    self.active? && nested_groups == 'on_members'
  end

  def nested_on_parents?
    self.active? && nested_groups == 'on_parents'
  end

  def membership_on_groups?
    self.active? && group_membership == 'on_groups'
  end

  def membership_on_members?
    self.active? && group_membership == 'on_members'
  end

  def sync_user_fields?
    has_user_fields_to_sync?
  end

  def sync_group_fields?
    has_group_fields_to_sync?
  end

  def sync_dyngroups?
    has_dyngroups?
  end

  def dyngroups_enabled_with_ttl?
    dyngroups == 'enabled_with_ttl'
  end

  def sync_on_login?
    active? && has_sync_on_login?
  end

  def sync_groups_on_login?
    sync_on_login == 'user_fields_and_groups'
  end

  def sync_fields_on_login?
    has_sync_on_login?
  end

  # Returns the evaluated proc of the account disabled test
  def account_locked_proc
    @account_locked_proc ||= if has_account_locked_test?
      eval("lambda { |flags| #{account_locked_test} }")
    end
  end

  # Returns the evaluated regular expression of groupname pattern
  def groupname_regexp
    @groupname_regexp ||= /#{groupname_pattern}/i
  end

  # Returns an array of ldap attributes to used when syncing the user fields
  def user_ldap_attrs_to_sync(fields = user_fields_to_sync)
    (fields||[]).map {|f| user_ldap_attrs[f] || (send(f.to_sym) if respond_to?(f.to_sym)) }
  end

  # Returns an array of ldap attributes to used when syncing the group fields
  def group_ldap_attrs_to_sync
    (group_fields_to_sync||[]).map {|f| group_ldap_attrs[f] }
  end

  # Returns the ldap attributes for the given fields
  # (not valid for custom fields)
  def ldap_attributes(*names)
    names.map {|n| send(n) }
  end

  # Returns the group field name for the given ldap attribute
  def group_field(ldap_attr)
    ldap_attr = ldap_attr.to_s
    group_ldap_attrs.find {|(k, v)| v.downcase == ldap_attr }.try(:first)
  end

  # Returns the user field name for the given ldap attribute
  def user_field(ldap_attr)
    ldap_attr = ldap_attr.to_s
    result = @user_standard_ldap_attrs.find {|(k, v)| v.downcase == ldap_attr }.try(:first)
    result ||= user_ldap_attrs.find {|(k, v)| v.downcase == ldap_attr }.try(:first)
  end

  def test
    @ldap_test ||= LdapTest.new(self)
  end

  def ldap_filter
    auth_source_ldap.send :ldap_filter
  end

  def users_search_onelevel?
    users_search_scope == 'onelevel'
  end

  # Creates a new ldap setting for the given ldap authentication source
  def initialize(source)
    @attributes = HashWithIndifferentAccess.new

    self.auth_source_ldap = source
    @attributes.merge!(settings)
    @user_standard_ldap_attrs = User::STANDARD_FIELDS.each_with_object({}) {|f, h| h[f] = (send(f)||'').downcase }
  end

  def auth_source_ldap_id=(id)
    @auth_source_ldap_id = id
    source = AuthSourceLdap.find_by_id(id)
    self.auth_source_ldap = source unless source.nil?
  end

  def auth_source_ldap
    @auth_source_ldap
  end

  def auth_source_ldap=(source)
    @auth_source_ldap = source
    @auth_source_ldap_id  = source.id
    @attributes[:auth_source_ldap_id] = source.id
  end

  # Sets attributes from attrs that are safe
  # attrs is a Hash with string keys
  def safe_attributes=(attrs, user = User.current)
    if attrs.respond_to?(:to_unsafe_hash)
      attrs = attrs.to_unsafe_hash
    end
    @attributes.merge!(delete_unsafe_attributes(attrs, user))
  end

  def save
    return false if invalid?

    self.settings = delete_unsafe_attributes(@attributes, User.current)
  end

  # Disables this ldap auth source
  # A disabled ldap auth source will not be synchronized
  def disable!
    self.active = false
    self.settings = settings.merge(:active => false)
  end

  # Overriden to enable validation (see ActiveModel::Validations#read_attribute_for_validation)
  def read_attribute_for_validation(key)
    @attributes[key]
  end

  # LdapSettings are always persisted because its authsource exists
  # (see ActiveModel::Lint::Tests::test_persisted?)
  def persisted?
    true
  end

  # Returns the name of an attribute to be displayed on the edit page
  def self.human_attribute_name(attr, *args)
    attr = attr.to_s.sub(/_id$/, '')

    l("field_#{name.underscore.gsub('/', '_')}_#{attr}", :default => ["field_#{attr}".to_sym, attr])
  end

  # Find the ldap setting for a given ldap auth source
  def self.find_by_auth_source_ldap_id(id)
    return unless source = AuthSourceLdap.find_by_id(id)

    LdapSetting.new(source)
  end

  # Find all the available ldap settings
  def self.all(options = {})
    AuthSourceLdap.where(options).
      map {|source| find_by_auth_source_ldap_id(source.id) }.
      compact
  end

  protected

    def validate_account_locked_test
      if account_locked_test.present?
        eval "lambda { |flags| #{account_locked_test} }"
      end
    rescue Exception => e
      errors.add :account_locked_test, :invalid_expression, :error_message => e.message.gsub(/^(\(eval\):1: )?(.*?)(lambda.*|$)/m, '\2')
      Rails.logger.error "#{e.message}\n #{e.backtrace.join("\n ")}"
    end

    def validate_groupname_pattern
      /#{groupname_pattern}/ if groupname_pattern.present?
    rescue Exception => e
      errors.add :groupname_pattern, :invalid_regexp, :error_message => e.message
    end

    def validate_group_filter
      Net::LDAP::Filter.construct(group_search_filter) if group_search_filter.present?
    rescue Net::LDAP::Error
      errors.add :group_search_filter, :invalid
    end

    def validate_user_ldap_attrs
      validate_ldap_attrs user_ldap_attrs, UserCustomField.all
    end

    def validate_user_fields_to_sync
      validate_fields user_fields_to_sync, (User::STANDARD_FIELDS + UserCustomField.all), user_ldap_attrs
    end

    def validate_group_ldap_attrs
      validate_ldap_attrs group_ldap_attrs, GroupCustomField.all
    end

    def validate_group_fields_to_sync
      validate_fields group_fields_to_sync, GroupCustomField.all, group_ldap_attrs
    end

    def validate_ldap_attrs(ldap_attrs, fields)
      field_ids = fields.map {|f| f.id.to_s }
      ldap_attrs.each do |k, v|
        if !field_ids.include?(k)
          errors.add :user_group_fields, :invalid unless errors.added? :user_group_fields, :invalid

        elsif v.present? && v !~ /\A[a-z][a-z0-9-]*\z/i
          field_name = fields.find {|f| f.id == k.to_i }.name
          errors.add :base, :invalid_ldap_attribute, :field => field_name
        end
      end
    end

    def validate_fields(fields_to_sync, fields, attrs)
      fields_ids = fields.map {|f| f.is_a?(String) ? f : f.id.to_s }
      if (fields_to_sync - fields_ids).present?
        errors.add :user_group_fields, :invalid unless errors.added? :user_group_fields, :invalid
      end
      fields_to_sync.each do |f|
        if f =~ /\A\d+\z/ && attrs[f].blank?
          field_name = fields.find {|c| !c.is_a?(String) && c.id.to_s == f }.name
          errors.add :base, :must_have_ldap_attribute, :field => field_name
        end
      end
    end

  private

    def set_fields_to_sync
      self.user_fields_to_sync ||= []
      self.group_fields_to_sync ||= []
    end

    def set_ldap_attrs
      self.user_ldap_attrs ||= {}
      self.group_ldap_attrs ||= {}
    end

    def strip_names
      LDAP_ATTRIBUTES.each {|a| @attributes[a].strip! unless @attributes[a].nil? }
      CLASS_NAMES.each {|a| @attributes[a].strip! unless @attributes[a].nil? }
    end

    def attributes
      @attributes
    end

    def attribute(attr)
      @attributes[attr]
    end

    def attribute=(attr, value)
      @attributes[attr] = value
    end

    def attribute?(attr)
      [true, '1', 'yes'].include? @attributes[attr]
    end

    def has_attribute?(attr)
      self.send("#{attr}").present?
    end

    def self.settings(source)
      Setting.plugin_redmine_ldap_sync.fetch(source.id, HashWithIndifferentAccess.new)
    end

    def settings
      LdapSetting.settings(@auth_source_ldap)
    end

    def settings=(attrs)
      Setting.plugin_redmine_ldap_sync = Setting.plugin_redmine_ldap_sync.merge!(@auth_source_ldap.id => attrs)
    end
end