SystemDataBackendLdap.rb 5.25 KB
  require 'active_support/secure_random'
  require 'net/ldap'

  class SystemDataBackendLdap

    LDAP_USER_MAP = {
      :uid           => :name,
      :userpassword  => :pass,
      :uidnumber     => :uid,
      :gidnumber     => :gid,
      :loginshell    => :shell,
      :homedirectory => :home
    }

    LDAP_GROUP_MAP = {
      :cn        => :name,
      :gidnumber => :gid,
      :memberuid => :members
    }

    LDAP_MAILALIASPERSON_MAP = {
      :sn => :surname,
      :cn => :name
    }

    LDAP_MAP = {
      :User            => LDAP_USER_MAP,
      :Group           => LDAP_GROUP_MAP,
      :Site            => { :o => :name },
      :MailAliasRole   => { :cn => :user },
      :MailAliasPerson => LDAP_MAILALIASPERSON_MAP,
      :mailAccount     => { :homedirectory => :home }
    }

    LDAP_FILTER = {
      :User            => '(objectClass=posixAccount)',
      :Group           => '(objectClass=posixGroup)',
      :Site            => '(&(objectClass=organization)(!(o=hosting)))',
      :MailAliasRole   => '(&(objectClass=MailAlias)(objectClass=organizationalrole))',
      :MailAliasPerson => '(&(objectClass=MailAlias)(objectClass=person))',
      :MailAccount     => '(objectClass=mailAccount)'
    }

    LDAP_OBJECTCLASS = {
      :User            => [ 'account', 'posixAccount', 'shadowAccount' ],
      :Group           => 'posixGroup',
      :Site            => 'organization',
      :MailAliasRole   => [ 'organizationalRole', 'MailAlias' ],
      :MailAliasPerson => [ 'person', 'MailAlias' ],
      :MailAccount     => [ 'person', 'MailAccount' ]
    }

    LDAP_LAMBDA_USER = lambda do |entry|
      entry[:cn] = entry[:uid]
      entry[:shadowlastchange] = (Time::now.to_i/60/60/24).to_s
      entry[:replace] += ['shadowreplace'] if entry[:replace]
    end

    LDAP_LAMBDA = {
      :User => LDAP_LAMBDA_USER
    }

    def initialize(host, port, baseDn, args={})
      @baseDn    = baseDn
      @systemDn  = 'o=system,' + @baseDn
      @hostingDn = 'o=hosting,' + @baseDn

      @systemDn  = args[:systemDn] if args[:systemDn]
      @hostingDn = args[:hostingDn] if args[:hostingDn]

      @ldap      = Net::LDAP.new(:host => host, :port => port)
      @ldapData  = Hash.new
    end

    def load!(kind)
      @ldapData[kind] = Hash.new if ! @ldapData[kind]

      @ldapData[kind][:int] = @ldap.search(
        :base   => ldapBase(kind),
        :filter => Net::LDAP::Filter::construct(LDAP_FILTER[kind])
      )
    end

    def load(kind)
      load!(kind) if ! @ldapData[kind]

      @ldapData[kind][:ext] = @ldapData[kind][:int].map do |data|
        map = { :dn => :id }
        map.merge!(LDAP_MAP[kind]) if LDAP_MAP[kind]

        ydata = {}
        data.each do |key,value|
          ydata.merge!({ map[key] || key => value.size==1?value[0]:value.to_a })
        end
        ydata
      end if ! @ldapData[kind][:ext] && @ldapData[kind][:int]

      @ldapData[kind][:ext].each{|ydata| yield ydata} if @ldapData[kind][:ext]
  end

  def update(kind, data)
    map = {}
    map.merge!(LDAP_MAP[kind].invert) if LDAP_MAP[kind]

    odata = @ldapData[kind][:ext].find{|edata| edata[:id] == data[:id]}

    data.each do |key,value| 
      pat_key = map[key] ? map[key] : key
      if odata[:id] =~ /(^|, *)#{pat_key.to_s}=([^, ]+)/ && $2 != value
        return replace(kind, data)
      end
    end

    entry = Net::LDAP::Entry.new(data[:id])
    data = data.find_all{|key,value| value != odata[key]}
    data.delete(:id)

    replace = Array.new
    data.each do |key,value|
      key = map[key] if map[key]
      replace.push(key.to_s)
      entry[key] = value
    end

    if replace.empty?
      puts 'INFO: no changes'
    else
      entry[:changetype]  = 'modify'
      entry[:replace]     = replace
      LDAP_LAMBDA[kind].call(entry) if LDAP_LAMBDA[kind]

      puts entry.to_ldif
    end
  end

  def replace(kind, data)
    puts 'INFO: do replace'
    puts '----------------'
    odata = @ldapData[kind][:ext].find{|edata| edata[:id] == data[:id]}
    delete(odata)
    puts
    insert(kind, data)
    puts '----------------'
  end

  def delete(data)
    entry = Net::LDAP::Entry.new(data[:id])
    entry[:changetype] = 'delete'

    puts entry.to_ldif
  end

  def insert(kind, data)
    map = {}
    map.merge!(LDAP_MAP[kind].invert) if LDAP_MAP[kind]

    data.delete(:id)
    entry = Net::LDAP::Entry.new(ldapDn(kind, data))
    entry[:changetype]  = 'add'
    entry[:objectclass] = LDAP_OBJECTCLASS[kind]

    data.each do |key,value|
      key = map[key] if map[key]
      entry[key] = value
    end
    LDAP_LAMBDA[kind].call(entry) if LDAP_LAMBDA[kind]

    puts entry.to_ldif
  end

  private

  def ldapBase(kind)
    case(kind)
    when :User, :Group: @systemDn
    when :Site, :MailAliasRole, :MailAliasPerson, :MailAccount: @hostingDn
    end
  end

  def ldapDn(kind, data)
    case(kind)
    when :User
      "uid=#{data[:name]},ou=user,#{ldapBase(kind)}"
    when :Group
      "cn=#{data[:name]},ou=group,#{ldapBase(kind)}"
    when :Site
      "o=#{data[:name]},#{ldapBase(kind)}"
    when :MailAliasRole
      "cn=#{data[:user]},o=#{data[:mail].sub(/.*@/, '')},#{ldapBase(kind)}"
    when :MailAliasPerson
      "mail=#{data[:mail]},o=#{data[:mail].sub(/.*@/, '')},#{ldapBase(kind)}"
    when :MailAccount
      "mail=#{data[:mail]},o=#{data[:mail].sub(/.*@/, '')},#{ldapBase(kind)}"
    end
  end

end