#!/usr/bin/env ruby.ruby3.4

# Copyright 2025 Open Text
#
# ------------------------------------------------------------------------------
# The only warranties for products and services of Open Text and its
# affiliates and licensors (“Open Text”) are as may be set forth in the express
# warranty statements accompanying such products and services.  Nothing herein
# should be construed as constituting an additional warranty. Open Text shall not
# be liable for technical or editorial errors or omissions contained herein. The
# information contained herein is subject to change without notice.
#
# Except as specifically indicated otherwise, this document contains
# confidential information and a valid license is required for possession, use or
# copying. If this work is provided to the U.S. Government, consistent with FAR
# 12.211 and 12.212, Commercial Computer Software, Computer Software
# Documentation, and Technical Data for Commercial Items are licensed to the U.S.
# Government under vendor's standard commercial license.
# ------------------------------------------------------------------------------

# Copyright (C) [2007-2008] Novell, Inc.  All Rights Reserved.

# THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND TREATIES.
# IT MAY NOT BE USED, COPIED, DISTRIBUTED, DISCLOSED, ADAPTED, PERFORMED,
# DISPLAYED, COLLECTED, COMPILED, OR LINKED WITHOUT NOVELL'S PRIOR WRITTEN
# CONSENT.  USE OR EXPLOITATION OF THIS WORK WITHOUT AUTHORIZATION COULD
# SUBJECT THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
 
# NOVELL PROVIDES THE WORK "AS IS," WITHOUT ANY EXPRESS OR IMPLIED WARRANTY,
# INCLUDING WITHOUT THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE, AND NON-INFRINGEMENT. NOVELL, THE AUTHORS OF THE WORK,
# AND THE OWNERS OF COPYRIGHT IN THE WORK ARE NOT LIABLE FOR ANY CLAIM, DAMAGES,
# OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE,
# ARISING FROM, OUT OF, OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER
# DEALINGS IN THE WORK.
require 'yaml'
require 'rubygems'
require 'mig'
require 'monitor'
require 'logger'
require 'psych'

include Migration

SupportedAttrs = "dn equivalentToMe member objectClass"

def should_migrate_obj? obj
  if @obj_list.nil?
    @obj_list = []
    file = ""
    if @opt['E']
      file = @opt['E']
    else
      file = "/etc/opt/novell/migration/obj-exclude-list.conf"
    end

    File.open(file).each { |line|
      @obj_list << line.strip if line.strip != "" and line.strip[0,1] != '#'
    }
  end
  @obj_list.each { |elem| 
    return false if (obj =~ /#{elem}/i) == 0
  }
  return true
end

def should_migrate_group? grp
  return false if not should_migrate_obj?(grp)
  result = Migration.ldapsearch(@source, @full_username_src, grp, "", @passwd_src, false, "", @useSecure_src, @ldapPort_src)
  if result == nil
    # Error occurred in ldapsearch... return
    return false
  end
  if result.include?("rbsContext") or result.include?("rbsScope") or result.include?("rbsCollection")
    return false
  else
    return true
  end
end


def get_users search_dn, scope, first_name
  options = {}
  options[:password] = @passwd
  host = getHostString(@dest, @useSecure_dest, @ldapPort_dest)
  cmd = "LC_ALL=en_US;ldapsearch -LLL -b \"#{search_dn}\" -s #{scope} -H #{host} -x -D #{@full_username} -W \"#{first_name}\" dn"
  return executeLdapCmd(cmd, options)
end

def matchup owner
  puts "---" if @firstTime == true
  @outcnt = @outcnt + 1
  @firstTime = false
  if owner.casecmp(@srcServerObj) == 0
    puts "'#{owner}': '#{@destServerObj}'"
    return
  end

  foundPerfectMatch = false
  first_name, base_name = parse_name(owner)

  #search if exact user exists
  if ldap_exist?(@opt['d'], @full_username, owner, @useSecure_dest, @ldapPort_dest)
    print("'#{owner}': '#{owner}'\n")
    foundPerfectMatch = true
  end
  if !foundPerfectMatch
    result = get_users(@opt['k'], "sub", first_name)
    if result == nil
      # Error occured in ldapsearch...
      print_message(ERROR, $globalLdapErrorMsg)
      return
    end
    print("'#{owner}':")
    if not result[0].nil? and result[0] != ""
      encoded = false
      if result[0].include?("::")
        #the first match is encoded
        a, v = result[0].split("::",2)
        encoded = true
      else
        a, v = result[0].split(":", 2)
      end
      v.lstrip!
      i = 1
      while i < result.length and result[i].index(":") == nil
        v << result[i].strip
        i = i + 1
      end
      if encoded
        val = "#{Base64.decode64(v.strip)}"
      else
        val = "#{v.strip}"
      end
      val.gsub!(/"/,"\\\"")
      print(" '#{val.strip}'")
    end
    print("\n")
  end
end

def map_ldap owner, posixFlag = false
  lowner = owner.downcase

  if not @hash.has_key?(lowner) and should_migrate_obj?(owner)
    @hash[lowner] = true
    first_name, base_name = parse_name owner

    result = Migration.ldapsearch(@source, @full_username_src, base_name, first_name, @passwd_src, false, SupportedAttrs, @useSecure_src, @ldapPort_src)
    if result == nil
      # Error occured in ldapsearch...
      print_message(ERROR, $globalLdapErrorMsg)
      return 
    end
    
    person_object = false
    group_object = false
    hattrs = Hash.new

    i = 0
    while i < result.length
      attrib = result[i]
      encoded = false
      if attrib.include?("::")
        a, v = attrib.split("::", 2)
        encoded = true
      else
        a, v = attrib.split(":",2)
      end
      v.strip!
      j = i + 1
      while (result[j] != nil and result[j].index(":") == nil)
        v << result[j][1, result[j].length].rstrip
        i = i + 1
        j = j + 1
      end
      if encoded
        val = Base64.decode64(v)
      else
        val = v
      end

      a.rstrip!
      val.gsub!(/"/,"\\\"")

      arr = nil
      if hattrs[a] == nil
        arr = Array.new
      else
        arr = hattrs[a]
      end
      hattrs[a] = arr << val

      person_object = true if v.include? "inetOrgPerson"
      group_object = true if v.include? "groupOfNames" or
                             v.include? "Organization" or
                             v.include? "organizationalUnit"

      i = i + 1
    end unless result == nil
    
    matchup(owner) if hattrs["dn"]

    if group_object 
      hattrs["member"].each do |member|
        unless @hash.has_key?(member.downcase)
          map_ldap(member)
        end
      end if hattrs.has_key? "member"

      hattrs["equivalentToMe"].each do |equiv|
        unless @hash.has_key?(equiv.downcase)
          map_ldap(equiv)
        end
      end if hattrs.has_key? "equivalentToMe"
    end
  end
end

def main
  $argdef = [
    ['s', 'source-server', "indicates the source LDAP server's IP address", ParseArgText, true],
    ['d', 'destination-server', "indicates the destination LDAP server's IP address", ParseArgText, true],
    ['k', 'destination-ldap-container', "option to specify LDAP container where all users and groups will be searched", ParseArgText, true],
    ['E', 'obj-exclude-file', "Exclude the objects listed in this file from migration", ParseArgText],
    ['c', 'session-file', "stores the migration progress, this file can be used to continue the migration", ParseArgText],
   
    [nil, 'source-unsecure-ldap', "use unsecure LDAP for all LDAP commands", ParseArgBool],
    [nil, 'source-ldap-port', "port where LDAP server is listening", ParseArgText],
    [nil, 'destination-unsecure-ldap', "use unsecure LDAP for all LDAP commands", ParseArgBool],
    [nil, 'destination-ldap-port', "port where destination LDAP server is listening", ParseArgText],
    [nil, 'progress', "display the progress in terms of percentage completed", ParseArgBool],
    [nil, 'progress-interval', "time interval for displaying progress", ParseArgText],
    [nil, 'use-casa', "use CASA to store/retrieve username and password information", ParseArgBool],
    [nil, 'precheck', "checks whether system meets all pre-requisite to start migmatchup", ParseArgBool],
    [nil, 'debug', "generate debug log", ParseArgBool],
    [nil, "inputfile", "the input file (usually produced by running 'mls'or 'ntuserls'). If not given on the commandline, will read from stdin", ParseArgOrd],
    ParseArgHelp,
    ParseArgUsage
  ]

  @init = true
  matchupCommand = "#{$0}"
  debugOpt = false
  $*.each do |arg|
    if arg.strip == "--debug"
      debugOpt = true
    end
    matchupCommand += " #{arg}"
  end
  initLog(debugOpt, "migmatchup")
  print_message(DEBUG, "migmatchup command started at #{`date +%d-%m-%y\\ %H:%M:%S`}")
  print_message(DEBUG, "migmatchup command executed as: #{matchupCommand}")

  @opt = {}
  (@opt, @extraargs) = parseargs(ARGV, $argdef)

  @opt['use-casa'] = true if ENV["MIG_USE_CASA"] != nil	

  @obj_list = nil
  filemap = @opt['inputfile']
  if filemap == nil
    filemap = $stdin
    if filemap.isatty
      print_message(FATAL, "Must specify input file or provide input to stdin")
      printUsage($argdef)
      exit 1
    end
  end
  if @opt['inputfile']
    if not File.exist?(@opt['inputfile']) or not File.size?(@opt['inputfile'])
      print_message(FATAL, "Specified input file #{@opt['inputfile']} does not exist or it is empty")
      exit 1
    end
  end
  if @opt['k']
    unless is_valid_ldap_container? @opt['k']
      print_message(FATAL, "Param --destination-ldap-container is not a valid ldap container")
      exit 1
    end
  end

  if filemap.class == String
    input = File.open filemap,"r" 
  else
    input = filemap
  end
  if filemap == $stdin and (@opt['progress'] or @opt['progress-interval'])
    print_message(FATAL, "Cannot specify --progress or --progress-interval when input is stdin")
    exit 1
  end 

  @hash = Hash.new
  @hash['""'] = true
  #don't migrate the special user like [public]
  @hash['[public]'] = true
  @hash['[root]'] = true
  @hash['[adminstrator]'] = true
  @hash['[supervisor]'] = true
  
  @firstTime = true

  if input != $stdin
    Psych.load_stream(input) do |doc|
      if doc != nil
        doc.each do |obj|
          if obj.class == FalseClass
            print_message(FATAL, "Input file not in correct format")
            exit 1
          end
          break
        end
        break
      end
    end
    input.seek(0, IO::SEEK_SET)
  end

  credentials = Migration::KeyStore.credential(@opt['s'], :ldap_dn, @opt['use-casa'])
  @full_username_src = "\"#{credentials[:username]}\""
  @source = @opt['s']
  @passwd_src = credentials[:password]

  credentials = nil
  credentials = Migration::KeyStore.credential(@opt['d'], :ldap_dn, @opt['use-casa'])
  @full_username = "\"#{credentials[:username]}\""
  @dest = @opt['d']
  @passwd = credentials[:password]

  if @opt['precheck']
    print_message(INFO, "Validation for migmatchup is done successfully")
    exit 0
  end
  @useSecure_src = true
  @useSecure_src = false if @opt['source-unsecure-ldap']
  @useSecure_dest = true
  @useSecure_dest = false if @opt['destination-unsecure-ldap']

  @ldapPort_src = nil
  @ldapPort_src = @opt['source-ldap-port'] if @opt['source-ldap-port']
  @ldapPort_dest = nil
  @ldapPort_dest = @opt['destination-ldap-port'] if @opt['destination-ldap-port']
 
  createLdapConfFile() 

  objNumber = 0
  @cnt = 0
  @outcnt = 0
  @jobDone = false
  if @opt['c']
    $session = "---\n"
    $session << "  started-on: #{`date`}"
    $session << "  src-server: #{@opt['s']}\n"
    $session << "  utility: migmatchup\n"
    if File.exist?(@opt['c']) and File.size?(@opt['c']) != 0
      sessionInput = File.open(@opt['c'], "r")
      sessionInfo = YAML.load(sessionInput)
      if sessionInfo['utility'] != "migmatchup" or
        sessionInfo['src-server'] != @opt['s']
        print_message(FATAL, "Invalid session file #{@opt['c']} specified. Please check the file")
        exit 1
      end
      if sessionInfo['status'] =~ /Completed/
        print_message(WARN, "Session file #{@opt['c']} shows migmatchup's status as 'Completed'")
        exit 0
      elsif sessionInfo['status'] =~ /Stopped/
        objNumber = sessionInfo['stopped-at'].to_i
      else
        print_message(FATAL, "Session file is corrupted")
        exit 1
      end
      sessionInput.close
    end
  end

  @init = false

  @srcServerObj = server_name(@opt['s']) + "," + server_context(@opt['s'])
  @destServerObj = server_name + "," + server_context

  if input != $stdin and @opt['progress']
    print_message(INFO, "precomputing for displaying progress")
    @totObjs = getTotalObjs(@opt['inputfile'])
    print_message(INFO, "Total number of objects to be processed is #{@totObjs}")
    if @opt['progress-interval']
      @progressInterval = @opt['progress-interval'].to_i
    else
      @progressInterval = 30
    end
    startTimer(@progressInterval)
  end
  if input == $stdin
    # We need to ignore the first "---"
    input.gets
  end
  Psych.load_stream(input) do |doc|
    if doc != nil
      doc.each do |obj|
        if @cnt >= objNumber
          map_ldap(obj['owner']) if obj.include? ('owner')
          map_ldap(obj['trustee']) if obj.include? ('trustee')
          map_ldap(obj['modifier']) if obj.include? ('modifier')
          if @cnt != 0 and @cnt % 20 == 0
            writeToSessionFile(@jobDone, @cnt) if @opt['c']
          end
          if @outcnt != 0 and @outcnt % NO_OF_OBJECTS_PER_RECORD == 0
            puts "---"
          end
        end
        @cnt = @cnt + 1
      end
    end
  end
  input.close if input.class == File
  if @opt['c']
    @jobDone = true
    writeToSessionFile(@jobDone, @cnt)
  end	
  deleteLdapConfFile()
  print_message(PROGRESS, "Processed #{@totObjs} trustees of #{@totObjs}") if @opt['progress']
  print_message(DEBUG, "migmatchup completed successfully at #{`date +%d-%m-%y\\ %H:%M:%S`}")
end

begin
  trap("ALRM"){
    print_message(PROGRESS, "Processed #{@cnt} trustees of #{@totObjs}")
    writeToSessionFile(@jobDone, @cnt) if @opt['c']
    startTimer(@progressInterval) if @opt['progress']
  }

  main

rescue SystemCallError => failed	
  print_message(FATAL,"SystemCallError, #{failed.message}")
  exit failed.errno
rescue MigrationException => e 
  print_message(FATAL,"MigrationException, #{e.message}")
  exit e.code 
rescue MigSyntaxError => e
  print_message(FATAL, "MigSyntaxError, " + e.to_s)
  printUsage($argdef)
  exit e.code
rescue LdapAuthError => e 
  print_message(FATAL,"LdapAuthError, #{e.message}")
  exit e.code 
rescue Interrupt
  print_message(DEBUG, "Interrupted by user")
  deleteLdapConfFile()
  writeToSessionFile(@jobDone, @cnt) if @opt['c'] and @init == false
  exit 0
rescue
  print_message(FATAL, "Caught exception: #{$!.message}\n Backtrace:\n #{$!.backtrace.join("\n")}")
  exit 1
ensure
  deleteLdapConfFile()
  $log.close if @opt['debug']
end
